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 ##
|
## Using ExoPlayer ##
|
||||||
|
|
||||||
|
ExoPlayer modules can be obtained via jCenter. It's also possible to clone the
|
||||||
|
repository and depend on the modules locally.
|
||||||
|
|
||||||
|
### Via jCenter ###
|
||||||
|
|
||||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||||
dependency. You need to make sure you have the jcenter repository included in
|
dependency. You need to make sure you have the jcenter repository included in
|
||||||
the `build.gradle` file in the root of your project:
|
the `build.gradle` file in the root of your project:
|
||||||
@ -64,6 +69,39 @@ latest versions, see the [Release notes][].
|
|||||||
[Bintray]: https://bintray.com/google/exoplayer
|
[Bintray]: https://bintray.com/google/exoplayer
|
||||||
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||||
|
|
||||||
|
### Locally ###
|
||||||
|
|
||||||
|
Cloning the repository and depending on the modules locally is required when
|
||||||
|
using some ExoPlayer extension modules. It's also a suitable approach if you
|
||||||
|
want to make local changes to ExoPlayer, or if you want to use a development
|
||||||
|
branch.
|
||||||
|
|
||||||
|
First, clone the repository into a local directory and checkout the desired
|
||||||
|
branch:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/google/ExoPlayer.git
|
||||||
|
git checkout release-v2
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, add the following to your project's `settings.gradle` file, replacing
|
||||||
|
`path/to/exoplayer` with the path to your local copy:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
gradle.ext.exoplayerRoot = 'path/to/exoplayer'
|
||||||
|
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
|
||||||
|
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
|
||||||
|
```
|
||||||
|
|
||||||
|
You should now see the ExoPlayer modules appear as part of your project. You can
|
||||||
|
depend on them as you would on any other local module, for example:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
compile project(':exoplayer-library-core')
|
||||||
|
compile project(':exoplayer-library-dash')
|
||||||
|
compile project(':exoplayer-library-ui)
|
||||||
|
```
|
||||||
|
|
||||||
## Developing ExoPlayer ##
|
## Developing ExoPlayer ##
|
||||||
|
|
||||||
#### Project branches ####
|
#### Project branches ####
|
||||||
|
@ -1,5 +1,60 @@
|
|||||||
# Release notes #
|
# Release notes #
|
||||||
|
|
||||||
|
### r2.4.3 ###
|
||||||
|
|
||||||
|
* Audio: Workaround custom audio decoders misreporting their maximum supported
|
||||||
|
channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)).
|
||||||
|
* Audio: Workaround for broken MediaTek raw decoder on some devices
|
||||||
|
([#2873](https://github.com/google/ExoPlayer/issues/2873)).
|
||||||
|
* Captions: Fix TTML captions appearing at the top of the screen
|
||||||
|
([#2953](https://github.com/google/ExoPlayer/issues/2953)).
|
||||||
|
* Captions: Fix handling of some DVB subtitles
|
||||||
|
([#2957](https://github.com/google/ExoPlayer/issues/2957)).
|
||||||
|
* Track selection: Fix setSelectionOverride(index, tracks, null)
|
||||||
|
([#2988](https://github.com/google/ExoPlayer/issues/2988)).
|
||||||
|
* GVR extension: Add support for mono input
|
||||||
|
([#2710](https://github.com/google/ExoPlayer/issues/2710)).
|
||||||
|
* FLAC extension: Fix failing build
|
||||||
|
([#2977](https://github.com/google/ExoPlayer/pull/2977)).
|
||||||
|
* Misc bugfixes.
|
||||||
|
|
||||||
|
### r2.4.2 ###
|
||||||
|
|
||||||
|
* Stability: Work around Nexus 10 reboot when playing certain content
|
||||||
|
([#2806](https://github.com/google/ExoPlayer/issues/2806)).
|
||||||
|
* MP3: Correctly treat MP3s with INFO headers as constant bitrate
|
||||||
|
([#2895](https://github.com/google/ExoPlayer/issues/2895)).
|
||||||
|
* HLS: Use average rather than peak bandwidth when available
|
||||||
|
([#2863](https://github.com/google/ExoPlayer/issues/2863)).
|
||||||
|
* SmoothStreaming: Fix timeline for live streams
|
||||||
|
([#2760](https://github.com/google/ExoPlayer/issues/2760)).
|
||||||
|
* UI: Fix DefaultTimeBar invalidation
|
||||||
|
([#2871](https://github.com/google/ExoPlayer/issues/2871)).
|
||||||
|
* Misc bugfixes.
|
||||||
|
|
||||||
|
### r2.4.1 ###
|
||||||
|
|
||||||
|
* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media
|
||||||
|
([#2780](https://github.com/google/ExoPlayer/issues/2780)).
|
||||||
|
* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec
|
||||||
|
input buffer allocations on all devices
|
||||||
|
([#2607](https://github.com/google/ExoPlayer/issues/2607)).
|
||||||
|
* Variable speed playback: Fix interpolation for rate/pitch adjustment
|
||||||
|
([#2774](https://github.com/google/ExoPlayer/issues/2774)).
|
||||||
|
* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist.
|
||||||
|
* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE
|
||||||
|
([#2743](https://github.com/google/ExoPlayer/issues/2743)).
|
||||||
|
* HLS: Correctly propagate errors loading the media playlist
|
||||||
|
([#2623](https://github.com/google/ExoPlayer/issues/2623)).
|
||||||
|
* UI: DefaultTimeBar enhancements and bug fixes
|
||||||
|
([#2740](https://github.com/google/ExoPlayer/issues/2740)).
|
||||||
|
* Ogg: Fix failure to play some Ogg files
|
||||||
|
([#2782](https://github.com/google/ExoPlayer/issues/2782)).
|
||||||
|
* Captions: Don't select text tack with no language by default.
|
||||||
|
* Captions: TTML positioning fixes
|
||||||
|
([#2824](https://github.com/google/ExoPlayer/issues/2824)).
|
||||||
|
* Misc bugfixes.
|
||||||
|
|
||||||
### r2.4.0 ###
|
### r2.4.0 ###
|
||||||
|
|
||||||
* New modular library structure. You can read more about depending on individual
|
* New modular library structure. You can read more about depending on individual
|
||||||
|
26
build.gradle
26
build.gradle
@ -16,7 +16,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:2.3.0'
|
classpath 'com.android.tools.build:gradle:2.3.1'
|
||||||
classpath 'com.novoda:bintray-release:0.4.0'
|
classpath 'com.novoda:bintray-release:0.4.0'
|
||||||
}
|
}
|
||||||
// Workaround for the following test coverage issue. Remove when fixed:
|
// Workaround for the following test coverage issue. Remove when fixed:
|
||||||
@ -33,23 +33,7 @@ allprojects {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
project.ext {
|
project.ext {
|
||||||
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
|
exoplayerPublishEnabled = true
|
||||||
// components provided by the library may be of use on older devices.
|
|
||||||
// However, please note that the core media playback functionality
|
|
||||||
// provided by the library requires API level 16 or greater.
|
|
||||||
minSdkVersion = 9
|
|
||||||
compileSdkVersion = 25
|
|
||||||
targetSdkVersion = 25
|
|
||||||
buildToolsVersion = '25'
|
|
||||||
testSupportLibraryVersion = '0.5'
|
|
||||||
supportLibraryVersion = '25.3.1'
|
|
||||||
dexmakerVersion = '1.2'
|
|
||||||
mockitoVersion = '1.9.5'
|
|
||||||
releaseRepoName = getBintrayRepo()
|
|
||||||
releaseUserOrg = 'google'
|
|
||||||
releaseGroupId = 'com.google.android.exoplayer'
|
|
||||||
releaseVersion = 'r2.4.0'
|
|
||||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
|
||||||
}
|
}
|
||||||
if (it.hasProperty('externalBuildDir')) {
|
if (it.hasProperty('externalBuildDir')) {
|
||||||
if (!new File(externalBuildDir).isAbsolute()) {
|
if (!new File(externalBuildDir).isAbsolute()) {
|
||||||
@ -59,10 +43,4 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getBintrayRepo() {
|
|
||||||
boolean publicRepo = hasProperty('publicRepo') &&
|
|
||||||
property('publicRepo').toBoolean()
|
|
||||||
return publicRepo ? 'exoplayer' : 'exoplayer-test'
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: 'javadoc_combined.gradle'
|
apply from: 'javadoc_combined.gradle'
|
||||||
|
32
constants.gradle
Normal file
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.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../constants.gradle'
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -45,13 +46,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
compile project(':library-dash')
|
compile project(modulePrefix + 'library-dash')
|
||||||
compile project(':library-hls')
|
compile project(modulePrefix + 'library-hls')
|
||||||
compile project(':library-smoothstreaming')
|
compile project(modulePrefix + 'library-smoothstreaming')
|
||||||
compile project(':library-ui')
|
compile project(modulePrefix + 'library-ui')
|
||||||
withExtensionsCompile project(path: ':extension-ffmpeg')
|
withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg')
|
||||||
withExtensionsCompile project(path: ':extension-flac')
|
withExtensionsCompile project(path: modulePrefix + 'extension-flac')
|
||||||
withExtensionsCompile project(path: ':extension-opus')
|
withExtensionsCompile project(path: modulePrefix + 'extension-ima')
|
||||||
withExtensionsCompile project(path: ':extension-vp9')
|
withExtensionsCompile project(path: modulePrefix + 'extension-opus')
|
||||||
|
withExtensionsCompile project(path: modulePrefix + 'extension-vp9')
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.google.android.exoplayer2.demo"
|
package="com.google.android.exoplayer2.demo"
|
||||||
android:versionCode="2400"
|
android:versionCode="2403"
|
||||||
android:versionName="2.4.0">
|
android:versionName="2.4.3">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
@ -138,28 +138,76 @@
|
|||||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
|
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "WV: Secure SD & HD (MP4,H264)",
|
"name": "WV: Secure SD & HD (cenc,MP4,H264)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||||
"drm_scheme": "widevine",
|
"drm_scheme": "widevine",
|
||||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "WV: Secure SD (MP4,H264)",
|
"name": "WV: Secure SD (cenc,MP4,H264)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||||
"drm_scheme": "widevine",
|
"drm_scheme": "widevine",
|
||||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "WV: Secure HD (MP4,H264)",
|
"name": "WV: Secure HD (cenc,MP4,H264)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_hd.mpd",
|
||||||
"drm_scheme": "widevine",
|
"drm_scheme": "widevine",
|
||||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "WV: Secure UHD (MP4,H264)",
|
"name": "WV: Secure UHD (cenc,MP4,H264)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||||
"drm_scheme": "widevine",
|
"drm_scheme": "widevine",
|
||||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure SD & HD (cbc1,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure SD (cbc1,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_sd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure HD (cbc1,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_hd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure UHD (cbc1,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1_uhd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure SD & HD (cbcs,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure SD (cbcs,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_sd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure HD (cbcs,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_hd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WV: Secure UHD (cbcs,MP4,H264)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -341,7 +389,7 @@
|
|||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Dizzy",
|
"name": "Dizzy",
|
||||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple AAC 10s",
|
"name": "Apple AAC 10s",
|
||||||
@ -353,7 +401,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Android screens (Matroska)",
|
"name": "Android screens (Matroska)",
|
||||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Big Buck Bunny (MP4 Video)",
|
"name": "Big Buck Bunny (MP4 Video)",
|
||||||
@ -377,7 +425,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Play (MP3 Audio)",
|
"name": "Google Play (MP3 Audio)",
|
||||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Play (Ogg/Vorbis Audio)",
|
"name": "Google Play (Ogg/Vorbis Audio)",
|
||||||
@ -408,10 +456,10 @@
|
|||||||
"name": "Cats -> Dogs",
|
"name": "Cats -> Dogs",
|
||||||
"playlist": [
|
"playlist": [
|
||||||
{
|
{
|
||||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -422,7 +470,7 @@
|
|||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||||
@ -435,13 +483,13 @@
|
|||||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||||
"playlist": [
|
"playlist": [
|
||||||
{
|
{
|
||||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uri": "http://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd"
|
||||||
@ -452,5 +500,85 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IMA sample ad tags",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "Single inline linear",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Single skippable inline",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Single redirect linear",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Single redirect error",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Single redirect broken (fallback)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-roll",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-roll + bumper",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP post-roll",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP post-roll + bumper",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-, mid- and post-rolls, single ads",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
|
||||||
|
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -95,6 +95,11 @@ import java.util.Locale;
|
|||||||
+ getStateString(state) + "]");
|
+ getStateString(state) + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) {
|
||||||
|
Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity() {
|
public void onPositionDiscontinuity() {
|
||||||
Log.d(TAG, "positionDiscontinuity");
|
Log.d(TAG, "positionDiscontinuity");
|
||||||
@ -276,7 +281,7 @@ import java.util.Locale;
|
|||||||
@Override
|
@Override
|
||||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||||
float pixelWidthHeightRatio) {
|
float pixelWidthHeightRatio) {
|
||||||
// Do nothing.
|
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -461,4 +466,16 @@ import java.util.Locale;
|
|||||||
return enabled ? "[X]" : "[ ]";
|
return enabled ? "[X]" : "[ ]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String getRepeatModeString(@ExoPlayer.RepeatMode int repeatMode) {
|
||||||
|
switch (repeatMode) {
|
||||||
|
case ExoPlayer.REPEAT_MODE_OFF:
|
||||||
|
return "OFF";
|
||||||
|
case ExoPlayer.REPEAT_MODE_ONE:
|
||||||
|
return "ONE";
|
||||||
|
case ExoPlayer.REPEAT_MODE_ALL:
|
||||||
|
return "ALL";
|
||||||
|
default:
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -26,7 +27,9 @@ import android.text.TextUtils;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -69,6 +72,7 @@ import com.google.android.exoplayer2.upstream.DataSource;
|
|||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
@ -92,6 +96,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||||
public static final String URI_LIST_EXTRA = "uri_list";
|
public static final String URI_LIST_EXTRA = "uri_list";
|
||||||
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
||||||
|
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||||
|
|
||||||
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
|
||||||
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
private static final CookieManager DEFAULT_COOKIE_MANAGER;
|
||||||
@ -200,10 +205,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
// Show the controls on any key event.
|
// If the event was not handled then see if the player view can handle it.
|
||||||
simpleExoPlayerView.showController();
|
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event);
|
||||||
// If the event was not handled then see if the player view can handle it as a media key event.
|
|
||||||
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnClickListener methods
|
// OnClickListener methods
|
||||||
@ -234,7 +237,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
boolean needNewPlayer = player == null;
|
boolean needNewPlayer = player == null;
|
||||||
if (needNewPlayer) {
|
if (needNewPlayer) {
|
||||||
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
|
TrackSelection.Factory adaptiveTrackSelectionFactory =
|
||||||
|
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
||||||
|
trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory);
|
||||||
|
trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory);
|
||||||
|
lastSeenTrackGroupArray = null;
|
||||||
|
eventLogger = new EventLogger(trackSelector);
|
||||||
|
|
||||||
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
|
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
|
||||||
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
|
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
|
||||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||||
@ -253,6 +262,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
|
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
|
||||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
((DemoApplication) getApplication()).useExtensionRenderers()
|
||||||
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||||
@ -261,16 +271,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
|
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this,
|
||||||
drmSessionManager, extensionRendererMode);
|
drmSessionManager, extensionRendererMode);
|
||||||
|
|
||||||
TrackSelection.Factory videoTrackSelectionFactory =
|
|
||||||
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
|
||||||
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
|
||||||
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
|
|
||||||
lastSeenTrackGroupArray = null;
|
|
||||||
|
|
||||||
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector);
|
||||||
player.addListener(this);
|
player.addListener(this);
|
||||||
|
|
||||||
eventLogger = new EventLogger(trackSelector);
|
|
||||||
player.addListener(eventLogger);
|
player.addListener(eventLogger);
|
||||||
player.setAudioDebugListener(eventLogger);
|
player.setAudioDebugListener(eventLogger);
|
||||||
player.setVideoDebugListener(eventLogger);
|
player.setVideoDebugListener(eventLogger);
|
||||||
@ -312,6 +314,26 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||||||
}
|
}
|
||||||
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
||||||
: new ConcatenatingMediaSource(mediaSources);
|
: new ConcatenatingMediaSource(mediaSources);
|
||||||
|
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
|
||||||
|
if (adTagUriString != null) {
|
||||||
|
Uri adTagUri = Uri.parse(adTagUriString);
|
||||||
|
ViewGroup adOverlayViewGroup = new FrameLayout(this);
|
||||||
|
// Load the extension source using reflection so that demo app doesn't have to depend on it.
|
||||||
|
try {
|
||||||
|
Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource");
|
||||||
|
Constructor<?> constructor = clazz.getConstructor(MediaSource.class,
|
||||||
|
DataSource.Factory.class, Context.class, Uri.class, ViewGroup.class);
|
||||||
|
mediaSource = (MediaSource) constructor.newInstance(mediaSource,
|
||||||
|
mediaDataSourceFactory, this, adTagUri, adOverlayViewGroup);
|
||||||
|
// The demo app has a non-null overlay frame layout.
|
||||||
|
simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup);
|
||||||
|
// Show a multi-window time bar, which will include ad position markers.
|
||||||
|
simpleExoPlayerView.setShowMultiWindowTimeBar(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Throw if the media source class was not found, or there was an error instantiating it.
|
||||||
|
showToast(R.string.ima_not_loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
||||||
if (haveResumePosition) {
|
if (haveResumePosition) {
|
||||||
player.seekTo(resumeWindow, resumePosition);
|
player.seekTo(resumeWindow, resumePosition);
|
||||||
@ -424,6 +446,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||||||
updateButtonVisibilities();
|
updateButtonVisibilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity() {
|
public void onPositionDiscontinuity() {
|
||||||
if (needRetrySource) {
|
if (needRetrySource) {
|
||||||
|
@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity {
|
|||||||
String[] drmKeyRequestProperties = null;
|
String[] drmKeyRequestProperties = null;
|
||||||
boolean preferExtensionDecoders = false;
|
boolean preferExtensionDecoders = false;
|
||||||
ArrayList<UriSample> playlistSamples = null;
|
ArrayList<UriSample> playlistSamples = null;
|
||||||
|
String adTagUri = null;
|
||||||
|
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity {
|
|||||||
}
|
}
|
||||||
reader.endArray();
|
reader.endArray();
|
||||||
break;
|
break;
|
||||||
|
case "ad_tag_uri":
|
||||||
|
adTagUri = reader.nextString();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ParserException("Unsupported attribute name: " + name);
|
throw new ParserException("Unsupported attribute name: " + name);
|
||||||
}
|
}
|
||||||
@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity {
|
|||||||
preferExtensionDecoders, playlistSamplesArray);
|
preferExtensionDecoders, playlistSamplesArray);
|
||||||
} else {
|
} else {
|
||||||
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
|
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
|
||||||
preferExtensionDecoders, uri, extension);
|
preferExtensionDecoders, uri, extension, adTagUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity {
|
|||||||
|
|
||||||
public final String uri;
|
public final String uri;
|
||||||
public final String extension;
|
public final String extension;
|
||||||
|
public final String adTagUri;
|
||||||
|
|
||||||
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
|
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
|
||||||
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
|
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
|
||||||
String extension) {
|
String extension, String adTagUri) {
|
||||||
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
|
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
|
this.adTagUri = adTagUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity {
|
|||||||
return super.buildIntent(context)
|
return super.buildIntent(context)
|
||||||
.setData(Uri.parse(uri))
|
.setData(Uri.parse(uri))
|
||||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||||
|
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
|
||||||
.setAction(PlayerActivity.ACTION_VIEW);
|
.setAction(PlayerActivity.ACTION_VIEW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,4 +58,6 @@
|
|||||||
|
|
||||||
<string name="sample_list_load_error">One or more sample lists failed to load</string>
|
<string name="sample_list_load_error">One or more sample lists failed to load</string>
|
||||||
|
|
||||||
|
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -11,13 +11,10 @@ The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][].
|
|||||||
|
|
||||||
## Build Instructions ##
|
## Build Instructions ##
|
||||||
|
|
||||||
* Checkout ExoPlayer along with Extensions:
|
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||||
|
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
```
|
[top level README][]. In addition, it's necessary to get the Cronet libraries
|
||||||
git clone https://github.com/google/ExoPlayer.git
|
and enable the extension:
|
||||||
```
|
|
||||||
|
|
||||||
* Get the Cronet libraries:
|
|
||||||
|
|
||||||
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
|
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
|
||||||
directory
|
directory
|
||||||
@ -27,6 +24,12 @@ git clone https://github.com/google/ExoPlayer.git
|
|||||||
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
|
||||||
directory of this extension
|
directory of this extension
|
||||||
|
|
||||||
* In ExoPlayer's `settings.gradle` file, uncomment the Cronet extension
|
* In your `settings.gradle` file, add the following line before the line that
|
||||||
|
applies `core_settings.gradle`:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
gradle.ext.exoplayerIncludeCronetExtension = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
|
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -29,11 +30,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
compile files('libs/cronet_api.jar')
|
compile files('libs/cronet_api.jar')
|
||||||
compile files('libs/cronet_impl_common_java.jar')
|
compile files('libs/cronet_impl_common_java.jar')
|
||||||
compile files('libs/cronet_impl_native_java.jar')
|
compile files('libs/cronet_impl_native_java.jar')
|
||||||
androidTestCompile project(':library')
|
androidTestCompile project(modulePrefix + 'library')
|
||||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
|
|
||||||
<instrumentation
|
<instrumentation
|
||||||
android:name="android.test.InstrumentationTestRunner"
|
android:name="android.test.InstrumentationTestRunner"
|
||||||
android:targetPackage="com.google.android.exoplayer.ext.cronet"
|
android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
|
||||||
tools:replace="android:targetPackage"/>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -16,9 +16,11 @@
|
|||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||||
|
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Predicate;
|
import com.google.android.exoplayer2.util.Predicate;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@ -34,43 +36,143 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||||||
*/
|
*/
|
||||||
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =
|
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =
|
||||||
CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
|
CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default read timeout, in milliseconds.
|
* The default read timeout, in milliseconds.
|
||||||
*/
|
*/
|
||||||
public static final int DEFAULT_READ_TIMEOUT_MILLIS =
|
public static final int DEFAULT_READ_TIMEOUT_MILLIS =
|
||||||
CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
|
CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
|
||||||
|
|
||||||
private final CronetEngine cronetEngine;
|
private final CronetEngineWrapper cronetEngineWrapper;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Predicate<String> contentTypePredicate;
|
private final Predicate<String> contentTypePredicate;
|
||||||
private final TransferListener<? super DataSource> transferListener;
|
private final TransferListener<? super DataSource> transferListener;
|
||||||
private final int connectTimeoutMs;
|
private final int connectTimeoutMs;
|
||||||
private final int readTimeoutMs;
|
private final int readTimeoutMs;
|
||||||
private final boolean resetTimeoutOnRedirects;
|
private final boolean resetTimeoutOnRedirects;
|
||||||
|
private final HttpDataSource.Factory fallbackFactory;
|
||||||
|
|
||||||
public CronetDataSourceFactory(CronetEngine cronetEngine,
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
* <p>
|
||||||
|
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||||
|
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||||
|
*
|
||||||
|
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
|
||||||
|
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||||
|
* cross-protocol redirects.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||||
|
* {@link CronetDataSource#open}.
|
||||||
|
* @param transferListener An optional listener.
|
||||||
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
|
||||||
|
* no suitable CronetEngine can be build.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor, Predicate<String> contentTypePredicate,
|
Executor executor, Predicate<String> contentTypePredicate,
|
||||||
TransferListener<? super DataSource> transferListener) {
|
TransferListener<? super DataSource> transferListener,
|
||||||
this(cronetEngine, executor, contentTypePredicate, transferListener,
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false);
|
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CronetDataSourceFactory(CronetEngine cronetEngine,
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
* <p>
|
||||||
|
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
|
||||||
|
* {@link DefaultHttpDataSourceFactory} will be used instead.
|
||||||
|
*
|
||||||
|
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
|
||||||
|
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
|
||||||
|
* cross-protocol redirects.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||||
|
* {@link CronetDataSource#open}.
|
||||||
|
* @param transferListener An optional listener.
|
||||||
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||||
|
Executor executor, Predicate<String> contentTypePredicate,
|
||||||
|
TransferListener<? super DataSource> transferListener, String userAgent) {
|
||||||
|
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
|
||||||
|
new DefaultHttpDataSourceFactory(userAgent, transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
* <p>
|
||||||
|
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
|
||||||
|
* {@link DefaultHttpDataSourceFactory} will be used instead.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||||
|
* {@link CronetDataSource#open}.
|
||||||
|
* @param transferListener An optional listener.
|
||||||
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor, Predicate<String> contentTypePredicate,
|
Executor executor, Predicate<String> contentTypePredicate,
|
||||||
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
||||||
int readTimeoutMs, boolean resetTimeoutOnRedirects) {
|
int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
|
||||||
this.cronetEngine = cronetEngine;
|
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
|
||||||
|
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
|
||||||
|
readTimeoutMs, resetTimeoutOnRedirects));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a CronetDataSourceFactory.
|
||||||
|
* <p>
|
||||||
|
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
|
||||||
|
* fallback {@link HttpDataSource.Factory} will be used instead.
|
||||||
|
*
|
||||||
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from
|
||||||
|
* {@link CronetDataSource#open}.
|
||||||
|
* @param transferListener An optional listener.
|
||||||
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
|
||||||
|
* no suitable CronetEngine can be build.
|
||||||
|
*/
|
||||||
|
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
|
||||||
|
Executor executor, Predicate<String> contentTypePredicate,
|
||||||
|
TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
|
||||||
|
int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||||
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
|
this.cronetEngineWrapper = cronetEngineWrapper;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
this.transferListener = transferListener;
|
this.transferListener = transferListener;
|
||||||
this.connectTimeoutMs = connectTimeoutMs;
|
this.connectTimeoutMs = connectTimeoutMs;
|
||||||
this.readTimeoutMs = readTimeoutMs;
|
this.readTimeoutMs = readTimeoutMs;
|
||||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||||
|
this.fallbackFactory = fallbackFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
|
protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
|
||||||
defaultRequestProperties) {
|
defaultRequestProperties) {
|
||||||
|
CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
|
||||||
|
if (cronetEngine == null) {
|
||||||
|
return fallbackFactory.createDataSource();
|
||||||
|
}
|
||||||
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
||||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
|
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
|
||||||
}
|
}
|
||||||
|
@ -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 ##
|
## Build instructions ##
|
||||||
|
|
||||||
* Checkout ExoPlayer along with Extensions
|
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||||
|
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
```
|
[top level README][]. In addition, it's necessary to build the extension's
|
||||||
git clone https://github.com/google/ExoPlayer.git
|
native components as follows:
|
||||||
```
|
|
||||||
|
|
||||||
* Set the following environment variables:
|
* Set the following environment variables:
|
||||||
|
|
||||||
@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
|
|||||||
|
|
||||||
* Download the [Android NDK][] and set its location in an environment variable:
|
* Download the [Android NDK][] and set its location in an environment variable:
|
||||||
|
|
||||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
|
||||||
|
|
||||||
```
|
```
|
||||||
NDK_PATH="<path to Android NDK>"
|
NDK_PATH="<path to Android NDK>"
|
||||||
```
|
```
|
||||||
@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
|
|||||||
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
|
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
|
||||||
```
|
```
|
||||||
|
|
||||||
* In your project, you can add a dependency on the extension by using a rule
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
like this:
|
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||||
|
|
||||||
```
|
|
||||||
// in settings.gradle
|
|
||||||
include ':..:ExoPlayer:library'
|
|
||||||
include ':..:ExoPlayer:extension-ffmpeg'
|
|
||||||
|
|
||||||
// in build.gradle
|
|
||||||
dependencies {
|
|
||||||
compile project(':..:ExoPlayer:library')
|
|
||||||
compile project(':..:ExoPlayer:extension-ffmpeg')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Now, when you build your app, the extension will be built and the native
|
|
||||||
libraries will be packaged along with the APK.
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -30,7 +31,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -10,11 +10,10 @@ ExoPlayer to play Flac audio on Android devices.
|
|||||||
|
|
||||||
## Build Instructions ##
|
## Build Instructions ##
|
||||||
|
|
||||||
* Checkout ExoPlayer along with Extensions:
|
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||||
|
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
```
|
[top level README][]. In addition, it's necessary to build the extension's
|
||||||
git clone https://github.com/google/ExoPlayer.git
|
native components as follows:
|
||||||
```
|
|
||||||
|
|
||||||
* Set the following environment variables:
|
* Set the following environment variables:
|
||||||
|
|
||||||
@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
|
|||||||
|
|
||||||
* Download the [Android NDK][] and set its location in an environment variable:
|
* Download the [Android NDK][] and set its location in an environment variable:
|
||||||
|
|
||||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
|
||||||
|
|
||||||
```
|
```
|
||||||
NDK_PATH="<path to Android NDK>"
|
NDK_PATH="<path to Android NDK>"
|
||||||
```
|
```
|
||||||
@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \
|
|||||||
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||||
```
|
```
|
||||||
|
|
||||||
* In your project, you can add a dependency to the Flac Extension by using a
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
rule like this:
|
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||||
|
|
||||||
```
|
|
||||||
// in settings.gradle
|
|
||||||
include ':..:ExoPlayer:library'
|
|
||||||
include ':..:ExoPlayer:extension-flac'
|
|
||||||
|
|
||||||
// in build.gradle
|
|
||||||
dependencies {
|
|
||||||
compile project(':..:ExoPlayer:library')
|
|
||||||
compile project(':..:ExoPlayer:extension-flac')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Now, when you build your app, the Flac extension will be built and the native
|
|
||||||
libraries will be packaged along with the APK.
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -30,8 +31,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
androidTestCompile project(':testutils')
|
androidTestCompile project(modulePrefix + 'testutils')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
|
|
||||||
<instrumentation
|
<instrumentation
|
||||||
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
|
android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
|
||||||
android:name="android.test.InstrumentationTestRunner"
|
android:name="android.test.InstrumentationTestRunner"/>
|
||||||
tools:replace="android:targetPackage"/>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link FlacExtractor}.
|
* Unit test for {@link FlacExtractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public class FlacExtractorTest extends InstrumentationTestCase {
|
public class FlacExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new FlacExtractor();
|
return new FlacExtractor();
|
||||||
|
@ -126,6 +126,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
private void releasePlayerAndQuitLooper() {
|
private void releasePlayerAndQuitLooper() {
|
||||||
player.release();
|
player.release();
|
||||||
Looper.myLooper().quit();
|
Looper.myLooper().quit();
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#define LOG_TAG "FLACParser"
|
#define LOG_TAG "FLACParser"
|
||||||
#define ALOGE(...) \
|
#define ALOGE(...) \
|
||||||
|
@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a
|
|||||||
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
|
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
|
||||||
of surround sound and ambisonic soundfields.
|
of surround sound and ambisonic soundfields.
|
||||||
|
|
||||||
## Using the extension ##
|
[Google VR SDK for Android]: https://developers.google.com/vr/android/
|
||||||
|
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
|
||||||
|
|
||||||
|
## Getting the extension ##
|
||||||
|
|
||||||
The easiest way to use the extension is to add it as a gradle dependency. You
|
The easiest way to use the extension is to add it as a gradle dependency. You
|
||||||
need to make sure you have the jcenter repository included in the `build.gradle`
|
need to make sure you have the jcenter repository included in the `build.gradle`
|
||||||
@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
|
|||||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||||
library being used.
|
library being used.
|
||||||
|
|
||||||
## Using GvrAudioProcessor ##
|
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||||
|
locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
|
[top level README][].
|
||||||
|
|
||||||
|
## Using the extension ##
|
||||||
|
|
||||||
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
|
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
|
||||||
return a GvrAudioProcessor.
|
return a GvrAudioProcessor.
|
||||||
* If constructing renderers directly, pass a GvrAudioProcessor to
|
* If constructing renderers directly, pass a GvrAudioProcessor to
|
||||||
MediaCodecAudioRenderer's constructor.
|
MediaCodecAudioRenderer's constructor.
|
||||||
|
|
||||||
[Google VR SDK for Android]: https://developers.google.com/vr/android/
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -24,8 +25,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
compile 'com.google.vr:sdk-audio:1.30.0'
|
compile 'com.google.vr:sdk-audio:1.60.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -82,6 +82,9 @@ public final class GvrAudioProcessor implements AudioProcessor {
|
|||||||
maybeReleaseGvrAudioSurround();
|
maybeReleaseGvrAudioSurround();
|
||||||
int surroundFormat;
|
int surroundFormat;
|
||||||
switch (channelCount) {
|
switch (channelCount) {
|
||||||
|
case 1:
|
||||||
|
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO;
|
||||||
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
|
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
|
||||||
break;
|
break;
|
||||||
|
43
extensions/ima/README.md
Normal file
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
|
The OkHttp Extension is an [HttpDataSource][] implementation using Square's
|
||||||
[OkHttp][].
|
[OkHttp][].
|
||||||
|
|
||||||
## Using the extension ##
|
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||||
|
[OkHttp]: https://square.github.io/okhttp/
|
||||||
|
|
||||||
The easiest way to use the extension is to add it as a gradle dependency. You
|
## Getting the extension ##
|
||||||
need to make sure you have the jcenter repository included in the `build.gradle`
|
|
||||||
file in the root of your project:
|
|
||||||
|
|
||||||
```gradle
|
The easiest way to use the extension is to add it as a gradle dependency:
|
||||||
repositories {
|
|
||||||
jcenter()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, include the following in your module's `build.gradle` file:
|
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
|
compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
|
||||||
@ -26,5 +19,8 @@ compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X'
|
|||||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||||
library being used.
|
library being used.
|
||||||
|
|
||||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
Alternatively, you can clone the ExoPlayer repository and depend on the module
|
||||||
[OkHttp]: https://square.github.io/okhttp/
|
locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
|
[top level README][].
|
||||||
|
|
||||||
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -29,7 +30,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
compile('com.squareup.okhttp3:okhttp:3.6.0') {
|
compile('com.squareup.okhttp3:okhttp:3.6.0') {
|
||||||
exclude group: 'org.json'
|
exclude group: 'org.json'
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
package com.google.android.exoplayer2.ext.okhttp;
|
package com.google.android.exoplayer2.ext.okhttp;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
@ -45,13 +47,14 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||||||
|
|
||||||
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
|
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
|
||||||
|
|
||||||
private final Call.Factory callFactory;
|
@NonNull private final Call.Factory callFactory;
|
||||||
private final String userAgent;
|
@NonNull private final RequestProperties requestProperties;
|
||||||
private final Predicate<String> contentTypePredicate;
|
|
||||||
private final TransferListener<? super OkHttpDataSource> listener;
|
@Nullable private final String userAgent;
|
||||||
private final CacheControl cacheControl;
|
@Nullable private final Predicate<String> contentTypePredicate;
|
||||||
private final RequestProperties defaultRequestProperties;
|
@Nullable private final TransferListener<? super OkHttpDataSource> listener;
|
||||||
private final RequestProperties requestProperties;
|
@Nullable private final CacheControl cacheControl;
|
||||||
|
@Nullable private final RequestProperties defaultRequestProperties;
|
||||||
|
|
||||||
private DataSpec dataSpec;
|
private DataSpec dataSpec;
|
||||||
private Response response;
|
private Response response;
|
||||||
@ -67,33 +70,34 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||||||
/**
|
/**
|
||||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
* by the source.
|
* by the source.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||||
Predicate<String> contentTypePredicate) {
|
@Nullable Predicate<String> contentTypePredicate) {
|
||||||
this(callFactory, userAgent, contentTypePredicate, null);
|
this(callFactory, userAgent, contentTypePredicate, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
* by the source.
|
* by the source.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
* predicate then a {@link InvalidContentTypeException} is thrown from
|
||||||
* {@link #open(DataSpec)}.
|
* {@link #open(DataSpec)}.
|
||||||
* @param listener An optional listener.
|
* @param listener An optional listener.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) {
|
@Nullable Predicate<String> contentTypePredicate,
|
||||||
|
@Nullable TransferListener<? super OkHttpDataSource> listener) {
|
||||||
this(callFactory, userAgent, contentTypePredicate, listener, null, null);
|
this(callFactory, userAgent, contentTypePredicate, listener, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
* by the source.
|
* by the source.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
* predicate then a {@link InvalidContentTypeException} is thrown from
|
* predicate then a {@link InvalidContentTypeException} is thrown from
|
||||||
* {@link #open(DataSpec)}.
|
* {@link #open(DataSpec)}.
|
||||||
@ -102,11 +106,12 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||||||
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
|
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
|
||||||
* the server as HTTP headers on every request.
|
* the server as HTTP headers on every request.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
|
@Nullable Predicate<String> contentTypePredicate,
|
||||||
CacheControl cacheControl, RequestProperties defaultRequestProperties) {
|
@Nullable TransferListener<? super OkHttpDataSource> listener,
|
||||||
|
@Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) {
|
||||||
this.callFactory = Assertions.checkNotNull(callFactory);
|
this.callFactory = Assertions.checkNotNull(callFactory);
|
||||||
this.userAgent = Assertions.checkNotEmpty(userAgent);
|
this.userAgent = userAgent;
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.cacheControl = cacheControl;
|
this.cacheControl = cacheControl;
|
||||||
@ -280,7 +285,10 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||||||
}
|
}
|
||||||
builder.addHeader("Range", rangeRequest);
|
builder.addHeader("Range", rangeRequest);
|
||||||
}
|
}
|
||||||
builder.addHeader("User-Agent", userAgent);
|
if (userAgent != null) {
|
||||||
|
builder.addHeader("User-Agent", userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
if (!allowGzip) {
|
if (!allowGzip) {
|
||||||
builder.addHeader("Accept-Encoding", "identity");
|
builder.addHeader("Accept-Encoding", "identity");
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.okhttp;
|
package com.google.android.exoplayer2.ext.okhttp;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||||
@ -28,31 +30,32 @@ import okhttp3.Call;
|
|||||||
*/
|
*/
|
||||||
public final class OkHttpDataSourceFactory extends BaseFactory {
|
public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||||
|
|
||||||
private final Call.Factory callFactory;
|
@NonNull private final Call.Factory callFactory;
|
||||||
private final String userAgent;
|
@Nullable private final String userAgent;
|
||||||
private final TransferListener<? super DataSource> listener;
|
@Nullable private final TransferListener<? super DataSource> listener;
|
||||||
private final CacheControl cacheControl;
|
@Nullable private final CacheControl cacheControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
* by the sources created by the factory.
|
* by the sources created by the factory.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param listener An optional listener.
|
* @param listener An optional listener.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
|
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||||
TransferListener<? super DataSource> listener) {
|
@Nullable TransferListener<? super DataSource> listener) {
|
||||||
this(callFactory, userAgent, listener, null);
|
this(callFactory, userAgent, listener, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||||
* by the sources created by the factory.
|
* by the sources created by the factory.
|
||||||
* @param userAgent The User-Agent string that should be used.
|
* @param userAgent An optional User-Agent string.
|
||||||
* @param listener An optional listener.
|
* @param listener An optional listener.
|
||||||
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||||
*/
|
*/
|
||||||
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
|
public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent,
|
||||||
TransferListener<? super DataSource> listener, CacheControl cacheControl) {
|
@Nullable TransferListener<? super DataSource> listener,
|
||||||
|
@Nullable CacheControl cacheControl) {
|
||||||
this.callFactory = callFactory;
|
this.callFactory = callFactory;
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
@ -10,11 +10,10 @@ ExoPlayer to play Opus audio on Android devices.
|
|||||||
|
|
||||||
## Build Instructions ##
|
## Build Instructions ##
|
||||||
|
|
||||||
* Checkout ExoPlayer along with Extensions:
|
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||||
|
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
```
|
[top level README][]. In addition, it's necessary to build the extension's
|
||||||
git clone https://github.com/google/ExoPlayer.git
|
native components as follows:
|
||||||
```
|
|
||||||
|
|
||||||
* Set the following environment variables:
|
* Set the following environment variables:
|
||||||
|
|
||||||
@ -26,8 +25,6 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main"
|
|||||||
|
|
||||||
* Download the [Android NDK][] and set its location in an environment variable:
|
* Download the [Android NDK][] and set its location in an environment variable:
|
||||||
|
|
||||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
|
||||||
|
|
||||||
```
|
```
|
||||||
NDK_PATH="<path to Android NDK>"
|
NDK_PATH="<path to Android NDK>"
|
||||||
```
|
```
|
||||||
@ -52,23 +49,8 @@ cd "${OPUS_EXT_PATH}"/jni && \
|
|||||||
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||||
```
|
```
|
||||||
|
|
||||||
* In your project, you can add a dependency to the Opus Extension by using a
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
rule like this:
|
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||||
|
|
||||||
```
|
|
||||||
// in settings.gradle
|
|
||||||
include ':..:ExoPlayer:library'
|
|
||||||
include ':..:ExoPlayer:extension-opus'
|
|
||||||
|
|
||||||
// in build.gradle
|
|
||||||
dependencies {
|
|
||||||
compile project(':..:ExoPlayer:library')
|
|
||||||
compile project(':..:ExoPlayer:extension-opus')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Now, when you build your app, the Opus extension will be built and the native
|
|
||||||
libraries will be packaged along with the APK.
|
|
||||||
|
|
||||||
## Notes ##
|
## Notes ##
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -30,7 +31,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
|
|
||||||
<instrumentation
|
<instrumentation
|
||||||
android:targetPackage="com.google.android.exoplayer2.ext.opus.test"
|
android:targetPackage="com.google.android.exoplayer2.ext.opus.test"
|
||||||
android:name="android.test.InstrumentationTestRunner"
|
android:name="android.test.InstrumentationTestRunner"/>
|
||||||
tools:replace="android:targetPackage"/>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -126,6 +126,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
private void releasePlayerAndQuitLooper() {
|
private void releasePlayerAndQuitLooper() {
|
||||||
player.release();
|
player.release();
|
||||||
Looper.myLooper().quit();
|
Looper.myLooper().quit();
|
||||||
|
@ -10,11 +10,10 @@ VP9 video on Android devices.
|
|||||||
|
|
||||||
## Build Instructions ##
|
## Build Instructions ##
|
||||||
|
|
||||||
* Checkout ExoPlayer along with Extensions:
|
To use this extension you need to clone the ExoPlayer repository and depend on
|
||||||
|
its modules locally. Instructions for doing this can be found in ExoPlayer's
|
||||||
```
|
[top level README][]. In addition, it's necessary to build the extension's
|
||||||
git clone https://github.com/google/ExoPlayer.git
|
native components as follows:
|
||||||
```
|
|
||||||
|
|
||||||
* Set the following environment variables:
|
* Set the following environment variables:
|
||||||
|
|
||||||
@ -26,8 +25,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
|
|||||||
|
|
||||||
* Download the [Android NDK][] and set its location in an environment variable:
|
* Download the [Android NDK][] and set its location in an environment variable:
|
||||||
|
|
||||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
|
||||||
|
|
||||||
```
|
```
|
||||||
NDK_PATH="<path to Android NDK>"
|
NDK_PATH="<path to Android NDK>"
|
||||||
```
|
```
|
||||||
@ -66,23 +63,8 @@ cd "${VP9_EXT_PATH}"/jni && \
|
|||||||
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
${NDK_PATH}/ndk-build APP_ABI=all -j4
|
||||||
```
|
```
|
||||||
|
|
||||||
* In your project, you can add a dependency to the VP9 Extension by using a the
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
following rule:
|
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||||
|
|
||||||
```
|
|
||||||
// in settings.gradle
|
|
||||||
include ':..:ExoPlayer:library'
|
|
||||||
include ':..:ExoPlayer:extension-vp9'
|
|
||||||
|
|
||||||
// in build.gradle
|
|
||||||
dependencies {
|
|
||||||
compile project(':..:ExoPlayer:library')
|
|
||||||
compile project(':..:ExoPlayer:extension-vp9')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Now, when you build your app, the VP9 extension will be built and the native
|
|
||||||
libraries will be packaged along with the APK.
|
|
||||||
|
|
||||||
## Notes ##
|
## Notes ##
|
||||||
|
|
||||||
@ -94,4 +76,3 @@ dependencies {
|
|||||||
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
|
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
|
||||||
please note that `generate_libvpx_android_configs.sh` and the makefiles need
|
please note that `generate_libvpx_android_configs.sh` and the makefiles need
|
||||||
to be modified to work with arbitrary versions of libvpx and libyuv.
|
to be modified to work with arbitrary versions of libvpx and libyuv.
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -30,7 +31,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
|
|
||||||
<instrumentation
|
<instrumentation
|
||||||
android:targetPackage="com.google.android.exoplayer2.ext.vp9.test"
|
android:targetPackage="com.google.android.exoplayer2.ext.vp9.test"
|
||||||
android:name="android.test.InstrumentationTestRunner"
|
android:name="android.test.InstrumentationTestRunner"/>
|
||||||
tools:replace="android:targetPackage"/>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -158,6 +158,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
private void releasePlayerAndQuitLooper() {
|
private void releasePlayerAndQuitLooper() {
|
||||||
player.release();
|
player.release();
|
||||||
Looper.myLooper().quit();
|
Looper.myLooper().quit();
|
||||||
|
@ -20,6 +20,7 @@ import android.graphics.Canvas;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import com.google.android.exoplayer2.BaseRenderer;
|
import com.google.android.exoplayer2.BaseRenderer;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
@ -30,6 +31,7 @@ import com.google.android.exoplayer2.FormatHolder;
|
|||||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.drm.DrmSession;
|
import com.google.android.exoplayer2.drm.DrmSession;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
@ -38,12 +40,35 @@ import com.google.android.exoplayer2.util.TraceUtil;
|
|||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||||
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
|
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes and renders video using the native VP9 decoder.
|
* Decodes and renders video using the native VP9 decoder.
|
||||||
*/
|
*/
|
||||||
public final class LibvpxVideoRenderer extends BaseRenderer {
|
public final class LibvpxVideoRenderer extends BaseRenderer {
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
|
||||||
|
REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
|
||||||
|
private @interface ReinitializationState {}
|
||||||
|
/**
|
||||||
|
* The decoder does not need to be re-initialized.
|
||||||
|
*/
|
||||||
|
private static final int REINITIALIZATION_STATE_NONE = 0;
|
||||||
|
/**
|
||||||
|
* The input format has changed in a way that requires the decoder to be re-initialized, but we
|
||||||
|
* haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
|
||||||
|
* ensure that it outputs any remaining buffers before we release it.
|
||||||
|
*/
|
||||||
|
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
|
||||||
|
/**
|
||||||
|
* The input format has changed in a way that requires the decoder to be re-initialized, and we've
|
||||||
|
* signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
|
||||||
|
* end of stream signal to indicate that it has output any remaining buffers before we release it.
|
||||||
|
*/
|
||||||
|
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of a message that can be passed to an instance of this class via
|
* The type of a message that can be passed to an instance of this class via
|
||||||
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
|
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
|
||||||
@ -71,12 +96,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
private DecoderCounters decoderCounters;
|
private DecoderCounters decoderCounters;
|
||||||
private Format format;
|
private Format format;
|
||||||
private VpxDecoder decoder;
|
private VpxDecoder decoder;
|
||||||
private DecoderInputBuffer inputBuffer;
|
private VpxInputBuffer inputBuffer;
|
||||||
private VpxOutputBuffer outputBuffer;
|
private VpxOutputBuffer outputBuffer;
|
||||||
private VpxOutputBuffer nextOutputBuffer;
|
private VpxOutputBuffer nextOutputBuffer;
|
||||||
private DrmSession<ExoMediaCrypto> drmSession;
|
private DrmSession<ExoMediaCrypto> drmSession;
|
||||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||||
|
|
||||||
|
@ReinitializationState
|
||||||
|
private int decoderReinitializationState;
|
||||||
|
private boolean decoderReceivedBuffers;
|
||||||
|
|
||||||
private Bitmap bitmap;
|
private Bitmap bitmap;
|
||||||
private boolean renderedFirstFrame;
|
private boolean renderedFirstFrame;
|
||||||
private long joiningDeadlineMs;
|
private long joiningDeadlineMs;
|
||||||
@ -153,6 +182,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
|
flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
|
||||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||||
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
|
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -185,49 +215,25 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have a format.
|
// If we don't have a decoder yet, we need to instantiate one.
|
||||||
drmSession = pendingDrmSession;
|
maybeInitDecoder();
|
||||||
ExoMediaCrypto mediaCrypto = null;
|
|
||||||
if (drmSession != null) {
|
if (decoder != null) {
|
||||||
int drmSessionState = drmSession.getState();
|
try {
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
// Rendering loop.
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
TraceUtil.beginSection("drainAndFeed");
|
||||||
} else if (drmSessionState == DrmSession.STATE_OPENED
|
while (drainOutputBuffer(positionUs)) {}
|
||||||
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
|
while (feedInputBuffer()) {}
|
||||||
mediaCrypto = drmSession.getMediaCrypto();
|
|
||||||
} else {
|
|
||||||
// The drm session isn't open yet.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (decoder == null) {
|
|
||||||
// If we don't have a decoder yet, we need to instantiate one.
|
|
||||||
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
|
||||||
TraceUtil.beginSection("createVpxDecoder");
|
|
||||||
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
|
|
||||||
decoder.setOutputMode(outputMode);
|
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
} catch (VpxDecoderException e) {
|
||||||
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
codecInitializedTimestamp - codecInitializingTimestamp);
|
|
||||||
decoderCounters.decoderInitCount++;
|
|
||||||
}
|
}
|
||||||
TraceUtil.beginSection("drainAndFeed");
|
decoderCounters.ensureUpdated();
|
||||||
while (drainOutputBuffer(positionUs)) {}
|
|
||||||
while (feedInputBuffer()) {}
|
|
||||||
TraceUtil.endSection();
|
|
||||||
} catch (VpxDecoderException e) {
|
|
||||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
|
||||||
}
|
}
|
||||||
decoderCounters.ensureUpdated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException {
|
private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException,
|
||||||
if (outputStreamEnded) {
|
VpxDecoderException {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acquire outputBuffer either from nextOutputBuffer or from the decoder.
|
// Acquire outputBuffer either from nextOutputBuffer or from the decoder.
|
||||||
if (outputBuffer == null) {
|
if (outputBuffer == null) {
|
||||||
if (nextOutputBuffer != null) {
|
if (nextOutputBuffer != null) {
|
||||||
@ -247,15 +253,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (outputBuffer.isEndOfStream()) {
|
if (outputBuffer.isEndOfStream()) {
|
||||||
outputStreamEnded = true;
|
if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
|
||||||
outputBuffer.release();
|
// We're waiting to re-initialize the decoder, and have now processed all final buffers.
|
||||||
outputBuffer = null;
|
releaseDecoder();
|
||||||
|
maybeInitDecoder();
|
||||||
|
} else {
|
||||||
|
outputBuffer.release();
|
||||||
|
outputBuffer = null;
|
||||||
|
outputStreamEnded = true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
|
if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
|
||||||
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
|
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
|
||||||
if (outputBuffer.timeUs <= positionUs) {
|
if (isBufferLate(outputBuffer.timeUs - positionUs)) {
|
||||||
skipBuffer();
|
skipBuffer();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -280,23 +292,20 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the current frame should be dropped.
|
* Returns whether the current frame should be dropped.
|
||||||
*
|
*
|
||||||
* @param outputBufferTimeUs The timestamp of the current output buffer.
|
* @param outputBufferTimeUs The timestamp of the current output buffer.
|
||||||
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or
|
* @param nextOutputBufferTimeUs The timestamp of the next output buffer or
|
||||||
* {@link TIME_UNSET} if the next output buffer is unavailable.
|
* {@link C#TIME_UNSET} if the next output buffer is unavailable.
|
||||||
* @param positionUs The current playback position.
|
* @param positionUs The current playback position.
|
||||||
* @param joiningDeadlineMs The joining deadline.
|
* @param joiningDeadlineMs The joining deadline.
|
||||||
* @return Returns whether to drop the current output buffer.
|
* @return Returns whether to drop the current output buffer.
|
||||||
*/
|
*/
|
||||||
protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
|
protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
|
||||||
long positionUs, long joiningDeadlineMs) {
|
long positionUs, long joiningDeadlineMs) {
|
||||||
// Drop the frame if we're joining and are more than 30ms late, or if we have the next frame
|
return isBufferLate(outputBufferTimeUs - positionUs)
|
||||||
// and that's also late. Else we'll render what we have.
|
&& (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
|
||||||
return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000)
|
|
||||||
|| (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderBuffer() {
|
private void renderBuffer() {
|
||||||
@ -356,7 +365,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
|
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
|
||||||
if (inputStreamEnded) {
|
if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
|
||||||
|
|| inputStreamEnded) {
|
||||||
|
// We need to reinitialize the decoder or the input stream has ended.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,6 +378,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
|
||||||
|
inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||||
|
decoder.queueInputBuffer(inputBuffer);
|
||||||
|
inputBuffer = null;
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int result;
|
int result;
|
||||||
if (waitingForKeys) {
|
if (waitingForKeys) {
|
||||||
// We've already read an encrypted sample into buffer, and are waiting for keys.
|
// We've already read an encrypted sample into buffer, and are waiting for keys.
|
||||||
@ -394,36 +413,43 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
inputBuffer.flip();
|
inputBuffer.flip();
|
||||||
|
inputBuffer.colorInfo = formatHolder.format.colorInfo;
|
||||||
decoder.queueInputBuffer(inputBuffer);
|
decoder.queueInputBuffer(inputBuffer);
|
||||||
|
decoderReceivedBuffers = true;
|
||||||
decoderCounters.inputBufferCount++;
|
decoderCounters.inputBufferCount++;
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
|
||||||
if (drmSession == null) {
|
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int drmSessionState = drmSession.getState();
|
@DrmSession.State int drmSessionState = drmSession.getState();
|
||||||
if (drmSessionState == DrmSession.STATE_ERROR) {
|
if (drmSessionState == DrmSession.STATE_ERROR) {
|
||||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||||
}
|
}
|
||||||
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
|
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
|
||||||
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void flushDecoder() {
|
private void flushDecoder() throws ExoPlaybackException {
|
||||||
inputBuffer = null;
|
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
if (outputBuffer != null) {
|
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
|
||||||
outputBuffer.release();
|
releaseDecoder();
|
||||||
outputBuffer = null;
|
maybeInitDecoder();
|
||||||
|
} else {
|
||||||
|
inputBuffer = null;
|
||||||
|
if (outputBuffer != null) {
|
||||||
|
outputBuffer.release();
|
||||||
|
outputBuffer = null;
|
||||||
|
}
|
||||||
|
if (nextOutputBuffer != null) {
|
||||||
|
nextOutputBuffer.release();
|
||||||
|
nextOutputBuffer = null;
|
||||||
|
}
|
||||||
|
decoder.flush();
|
||||||
|
decoderReceivedBuffers = false;
|
||||||
}
|
}
|
||||||
if (nextOutputBuffer != null) {
|
|
||||||
nextOutputBuffer.release();
|
|
||||||
nextOutputBuffer = null;
|
|
||||||
}
|
|
||||||
decoder.flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -461,7 +487,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPositionReset(long positionUs, boolean joining) {
|
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||||
inputStreamEnded = false;
|
inputStreamEnded = false;
|
||||||
outputStreamEnded = false;
|
outputStreamEnded = false;
|
||||||
clearRenderedFirstFrame();
|
clearRenderedFirstFrame();
|
||||||
@ -480,18 +506,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
protected void onStarted() {
|
protected void onStarted() {
|
||||||
droppedFrames = 0;
|
droppedFrames = 0;
|
||||||
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
|
droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
|
||||||
joiningDeadlineMs = C.TIME_UNSET;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStopped() {
|
protected void onStopped() {
|
||||||
|
joiningDeadlineMs = C.TIME_UNSET;
|
||||||
maybeNotifyDroppedFrames();
|
maybeNotifyDroppedFrames();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDisabled() {
|
protected void onDisabled() {
|
||||||
inputBuffer = null;
|
|
||||||
outputBuffer = null;
|
|
||||||
format = null;
|
format = null;
|
||||||
waitingForKeys = false;
|
waitingForKeys = false;
|
||||||
clearReportedVideoSize();
|
clearReportedVideoSize();
|
||||||
@ -518,20 +542,53 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseDecoder() {
|
private void maybeInitDecoder() throws ExoPlaybackException {
|
||||||
if (decoder != null) {
|
if (decoder != null) {
|
||||||
decoder.release();
|
return;
|
||||||
decoder = null;
|
}
|
||||||
decoderCounters.decoderReleaseCount++;
|
|
||||||
waitingForKeys = false;
|
drmSession = pendingDrmSession;
|
||||||
if (drmSession != null && pendingDrmSession != drmSession) {
|
ExoMediaCrypto mediaCrypto = null;
|
||||||
try {
|
if (drmSession != null) {
|
||||||
drmSessionManager.releaseSession(drmSession);
|
mediaCrypto = drmSession.getMediaCrypto();
|
||||||
} finally {
|
if (mediaCrypto == null) {
|
||||||
drmSession = null;
|
DrmSessionException drmError = drmSession.getError();
|
||||||
|
if (drmError != null) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(drmError, getIndex());
|
||||||
}
|
}
|
||||||
|
// The drm session isn't open yet.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
||||||
|
TraceUtil.beginSection("createVpxDecoder");
|
||||||
|
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
|
||||||
|
decoder.setOutputMode(outputMode);
|
||||||
|
TraceUtil.endSection();
|
||||||
|
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
||||||
|
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
|
||||||
|
codecInitializedTimestamp - codecInitializingTimestamp);
|
||||||
|
decoderCounters.decoderInitCount++;
|
||||||
|
} catch (VpxDecoderException e) {
|
||||||
|
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDecoder() {
|
||||||
|
if (decoder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputBuffer = null;
|
||||||
|
outputBuffer = null;
|
||||||
|
nextOutputBuffer = null;
|
||||||
|
decoder.release();
|
||||||
|
decoder = null;
|
||||||
|
decoderCounters.decoderReleaseCount++;
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
|
decoderReceivedBuffers = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||||
@ -555,6 +612,17 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pendingDrmSession != drmSession) {
|
||||||
|
if (decoderReceivedBuffers) {
|
||||||
|
// Signal end of stream and wait for any final output buffers before re-initialization.
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
|
||||||
|
} else {
|
||||||
|
// There aren't any final output buffers, so release the decoder immediately.
|
||||||
|
releaseDecoder();
|
||||||
|
maybeInitDecoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventDispatcher.inputFormatChanged(format);
|
eventDispatcher.inputFormatChanged(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,4 +722,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isBufferLate(long earlyUs) {
|
||||||
|
// Class a buffer as late if it should have been presented more than 30ms ago.
|
||||||
|
return earlyUs < -30000;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9;
|
|||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
import com.google.android.exoplayer2.decoder.CryptoInfo;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
|
||||||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||||
import com.google.android.exoplayer2.drm.DecryptionException;
|
import com.google.android.exoplayer2.drm.DecryptionException;
|
||||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||||
@ -27,7 +26,7 @@ import java.nio.ByteBuffer;
|
|||||||
* Vpx decoder.
|
* Vpx decoder.
|
||||||
*/
|
*/
|
||||||
/* package */ final class VpxDecoder extends
|
/* package */ final class VpxDecoder extends
|
||||||
SimpleDecoder<DecoderInputBuffer, VpxOutputBuffer, VpxDecoderException> {
|
SimpleDecoder<VpxInputBuffer, VpxOutputBuffer, VpxDecoderException> {
|
||||||
|
|
||||||
public static final int OUTPUT_MODE_NONE = -1;
|
public static final int OUTPUT_MODE_NONE = -1;
|
||||||
public static final int OUTPUT_MODE_YUV = 0;
|
public static final int OUTPUT_MODE_YUV = 0;
|
||||||
@ -54,7 +53,7 @@ import java.nio.ByteBuffer;
|
|||||||
*/
|
*/
|
||||||
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
|
||||||
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
|
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
|
||||||
super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
|
||||||
if (!VpxLibrary.isAvailable()) {
|
if (!VpxLibrary.isAvailable()) {
|
||||||
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
throw new VpxDecoderException("Failed to load decoder native libraries.");
|
||||||
}
|
}
|
||||||
@ -85,8 +84,8 @@ import java.nio.ByteBuffer;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected DecoderInputBuffer createInputBuffer() {
|
protected VpxInputBuffer createInputBuffer() {
|
||||||
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
|
return new VpxInputBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -100,7 +99,7 @@ import java.nio.ByteBuffer;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected VpxDecoderException decode(DecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
|
protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
|
||||||
boolean reset) {
|
boolean reset) {
|
||||||
ByteBuffer inputData = inputBuffer.data;
|
ByteBuffer inputData = inputBuffer.data;
|
||||||
int inputSize = inputData.limit();
|
int inputSize = inputData.limit();
|
||||||
@ -128,6 +127,7 @@ import java.nio.ByteBuffer;
|
|||||||
} else if (getFrameResult == -1) {
|
} else if (getFrameResult == -1) {
|
||||||
return new VpxDecoderException("Buffer initialization failed.");
|
return new VpxDecoderException("Buffer initialization failed.");
|
||||||
}
|
}
|
||||||
|
outputBuffer.colorInfo = inputBuffer.colorInfo;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
package com.google.android.exoplayer2.ext.vp9;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.decoder.OutputBuffer;
|
import com.google.android.exoplayer2.decoder.OutputBuffer;
|
||||||
|
import com.google.android.exoplayer2.video.ColorInfo;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,6 +38,8 @@ import java.nio.ByteBuffer;
|
|||||||
public ByteBuffer data;
|
public ByteBuffer data;
|
||||||
public int width;
|
public int width;
|
||||||
public int height;
|
public int height;
|
||||||
|
public ColorInfo colorInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* YUV planes for YUV mode.
|
* YUV planes for YUV mode.
|
||||||
*/
|
*/
|
||||||
|
@ -51,8 +51,7 @@ config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx"
|
|||||||
config[3]+=" --disable-avx2 --enable-pic"
|
config[3]+=" --disable-avx2 --enable-pic"
|
||||||
|
|
||||||
arch[4]="arm64-v8a"
|
arch[4]="arm64-v8a"
|
||||||
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon"
|
config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon"
|
||||||
config[4]+=" --disable-neon-asm"
|
|
||||||
|
|
||||||
arch[5]="x86_64"
|
arch[5]="x86_64"
|
||||||
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
|
config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2"
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -24,11 +25,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':library-core')
|
compile project(modulePrefix + 'library-core')
|
||||||
compile project(':library-dash')
|
compile project(modulePrefix + 'library-dash')
|
||||||
compile project(':library-hls')
|
compile project(modulePrefix + 'library-hls')
|
||||||
compile project(':library-smoothstreaming')
|
compile project(modulePrefix + 'library-smoothstreaming')
|
||||||
compile project(':library-ui')
|
compile project(modulePrefix + 'library-ui')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
@ -22,6 +23,7 @@ android {
|
|||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround to prevent circular dependency on project :testutils.
|
||||||
sourceSets {
|
sourceSets {
|
||||||
androidTest {
|
androidTest {
|
||||||
java.srcDirs += "../../testutils/src/main/java/"
|
java.srcDirs += "../../testutils/src/main/java/"
|
||||||
|
@ -24,11 +24,13 @@
|
|||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||||
<uses-library android:name="android.test.runner"/>
|
<uses-library android:name="android.test.runner"/>
|
||||||
|
<provider
|
||||||
|
android:authorities="com.google.android.exoplayer2.core.test"
|
||||||
|
android:name="com.google.android.exoplayer2.upstream.ContentDataSourceTest$TestContentProvider"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<instrumentation
|
<instrumentation
|
||||||
android:targetPackage="com.google.android.exoplayer2.core.test"
|
android:targetPackage="com.google.android.exoplayer2.core.test"
|
||||||
android:name="android.test.InstrumentationTestRunner"
|
android:name="android.test.InstrumentationTestRunner"/>
|
||||||
tools:replace="android:targetPackage"/>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
Binary file not shown.
@ -30,5 +30,6 @@ track 1:
|
|||||||
time = 0
|
time = 0
|
||||||
flags = 1073741824
|
flags = 1073741824
|
||||||
data = length 39, hash B7FE77F4
|
data = length 39, hash B7FE77F4
|
||||||
|
crypto mode = 1
|
||||||
encryption key = length 16, hash 4CE944CF
|
encryption key = length 16, hash 4CE944CF
|
||||||
tracksEnded = true
|
tracksEnded = true
|
||||||
|
@ -30,5 +30,6 @@ track 1:
|
|||||||
time = 0
|
time = 0
|
||||||
flags = 1073741824
|
flags = 1073741824
|
||||||
data = length 24, hash E58668B1
|
data = length 24, hash E58668B1
|
||||||
|
crypto mode = 1
|
||||||
encryption key = length 16, hash 4CE944CF
|
encryption key = length 16, hash 4CE944CF
|
||||||
tracksEnded = true
|
tracksEnded = true
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -8,7 +8,7 @@ track 0:
|
|||||||
bitrate = -1
|
bitrate = -1
|
||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/x-flac
|
sampleMimeType = audio/flac
|
||||||
maxInputSize = 768000
|
maxInputSize = 768000
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
|
@ -9,7 +9,7 @@ track 0:
|
|||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/vorbis
|
sampleMimeType = audio/vorbis
|
||||||
maxInputSize = 65025
|
maxInputSize = -1
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
|
@ -9,7 +9,7 @@ track 0:
|
|||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/vorbis
|
sampleMimeType = audio/vorbis
|
||||||
maxInputSize = 65025
|
maxInputSize = -1
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
|
@ -9,7 +9,7 @@ track 0:
|
|||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/vorbis
|
sampleMimeType = audio/vorbis
|
||||||
maxInputSize = 65025
|
maxInputSize = -1
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
|
@ -9,7 +9,7 @@ track 0:
|
|||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/vorbis
|
sampleMimeType = audio/vorbis
|
||||||
maxInputSize = 65025
|
maxInputSize = -1
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
|
@ -9,7 +9,7 @@ track 0:
|
|||||||
id = null
|
id = null
|
||||||
containerMimeType = null
|
containerMimeType = null
|
||||||
sampleMimeType = audio/vorbis
|
sampleMimeType = audio/vorbis
|
||||||
maxInputSize = 65025
|
maxInputSize = -1
|
||||||
width = -1
|
width = -1
|
||||||
height = -1
|
height = -1
|
||||||
frameRate = -1.0
|
frameRate = -1.0
|
||||||
|
@ -15,28 +15,20 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.SampleStream;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.testutil.FakeRenderer;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||||
import com.google.android.exoplayer2.util.MediaClock;
|
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,7 +54,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||||||
* error.
|
* error.
|
||||||
*/
|
*/
|
||||||
public void testPlayEmptyTimeline() throws Exception {
|
public void testPlayEmptyTimeline() throws Exception {
|
||||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||||
Timeline timeline = Timeline.EMPTY;
|
Timeline timeline = Timeline.EMPTY;
|
||||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||||
FakeRenderer renderer = new FakeRenderer(null);
|
FakeRenderer renderer = new FakeRenderer(null);
|
||||||
@ -79,7 +71,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||||||
* Tests playback of a source that exposes a single period.
|
* Tests playback of a source that exposes a single period.
|
||||||
*/
|
*/
|
||||||
public void testPlaySinglePeriodTimeline() throws Exception {
|
public void testPlaySinglePeriodTimeline() throws Exception {
|
||||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||||
Object manifest = new Object();
|
Object manifest = new Object();
|
||||||
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
|
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
|
||||||
@ -98,7 +90,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||||||
* Tests playback of a source that exposes three periods.
|
* Tests playback of a source that exposes three periods.
|
||||||
*/
|
*/
|
||||||
public void testPlayMultiPeriodTimeline() throws Exception {
|
public void testPlayMultiPeriodTimeline() throws Exception {
|
||||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||||
Timeline timeline = new FakeTimeline(
|
Timeline timeline = new FakeTimeline(
|
||||||
new TimelineWindowDefinition(false, false, 0),
|
new TimelineWindowDefinition(false, false, 0),
|
||||||
new TimelineWindowDefinition(false, false, 0),
|
new TimelineWindowDefinition(false, false, 0),
|
||||||
@ -119,7 +111,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||||||
* source.
|
* source.
|
||||||
*/
|
*/
|
||||||
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
|
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
|
||||||
final PlayerWrapper playerWrapper = new PlayerWrapper();
|
final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||||
Timeline timeline = new FakeTimeline(
|
Timeline timeline = new FakeTimeline(
|
||||||
new TimelineWindowDefinition(false, false, 10),
|
new TimelineWindowDefinition(false, false, 10),
|
||||||
new TimelineWindowDefinition(false, false, 10),
|
new TimelineWindowDefinition(false, false, 10),
|
||||||
@ -166,7 +158,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void testRepreparationGivesFreshSourceInfo() throws Exception {
|
public void testRepreparationGivesFreshSourceInfo() throws Exception {
|
||||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||||
|
|
||||||
@ -218,501 +210,54 @@ public final class ExoPlayerTest extends TestCase {
|
|||||||
Pair.create(timeline, thirdSourceManifest));
|
Pair.create(timeline, thirdSourceManifest));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void testRepeatModeChanges() throws Exception {
|
||||||
* Wraps a player with its own handler thread.
|
Timeline timeline = new FakeTimeline(
|
||||||
*/
|
new TimelineWindowDefinition(true, false, 100000),
|
||||||
private static final class PlayerWrapper implements ExoPlayer.EventListener {
|
new TimelineWindowDefinition(true, false, 100000),
|
||||||
|
new TimelineWindowDefinition(true, false, 100000));
|
||||||
private final CountDownLatch sourceInfoCountDownLatch;
|
final int[] actionSchedule = { // 0 -> 1
|
||||||
private final CountDownLatch endedCountDownLatch;
|
ExoPlayer.REPEAT_MODE_ONE, // 1 -> 1
|
||||||
private final HandlerThread playerThread;
|
ExoPlayer.REPEAT_MODE_OFF, // 1 -> 2
|
||||||
private final Handler handler;
|
ExoPlayer.REPEAT_MODE_ONE, // 2 -> 2
|
||||||
private final LinkedList<Pair<Timeline, Object>> sourceInfos;
|
ExoPlayer.REPEAT_MODE_ALL, // 2 -> 0
|
||||||
|
ExoPlayer.REPEAT_MODE_ONE, // 0 -> 0
|
||||||
private ExoPlayer player;
|
-1, // 0 -> 0
|
||||||
private TrackGroupArray trackGroups;
|
ExoPlayer.REPEAT_MODE_OFF, // 0 -> 1
|
||||||
private Exception exception;
|
-1, // 1 -> 2
|
||||||
|
-1 // 2 -> ended
|
||||||
// Written only on the main thread.
|
};
|
||||||
private volatile int positionDiscontinuityCount;
|
int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
|
||||||
|
final LinkedList<Integer> windowIndices = new LinkedList<>();
|
||||||
public PlayerWrapper() {
|
final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
|
||||||
sourceInfoCountDownLatch = new CountDownLatch(1);
|
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
|
||||||
endedCountDownLatch = new CountDownLatch(1);
|
@Override
|
||||||
playerThread = new HandlerThread("ExoPlayerTest thread");
|
@SuppressWarnings("ResourceType")
|
||||||
playerThread.start();
|
public void onPositionDiscontinuity() {
|
||||||
handler = new Handler(playerThread.getLooper());
|
super.onPositionDiscontinuity();
|
||||||
sourceInfos = new LinkedList<>();
|
int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
|
||||||
}
|
if (actionSchedule[actionIndex] != -1) {
|
||||||
|
player.setRepeatMode(actionSchedule[actionIndex]);
|
||||||
// Called on the test thread.
|
|
||||||
|
|
||||||
public void blockUntilEnded(long timeoutMs) throws Exception {
|
|
||||||
if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
|
|
||||||
exception = new TimeoutException("Test playback timed out waiting for playback to end.");
|
|
||||||
}
|
|
||||||
release();
|
|
||||||
// Throw any pending exception (from playback, timing out or releasing).
|
|
||||||
if (exception != null) {
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception {
|
|
||||||
if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
|
|
||||||
throw new TimeoutException("Test playback timed out waiting for source info.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setup(final MediaSource mediaSource, final Renderer... renderers) {
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
|
|
||||||
player.addListener(PlayerWrapper.this);
|
|
||||||
player.setPlayWhenReady(true);
|
|
||||||
player.prepare(mediaSource);
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
windowIndices.add(player.getCurrentWindowIndex());
|
||||||
}
|
actionCounter.countDown();
|
||||||
|
|
||||||
public void prepare(final MediaSource mediaSource) {
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
player.prepare(mediaSource);
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void release() throws InterruptedException {
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
if (player != null) {
|
|
||||||
player.release();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleError(e);
|
|
||||||
} finally {
|
|
||||||
playerThread.quit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
playerThread.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleError(Exception exception) {
|
|
||||||
if (this.exception == null) {
|
|
||||||
this.exception = exception;
|
|
||||||
}
|
}
|
||||||
endedCountDownLatch.countDown();
|
};
|
||||||
|
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
|
||||||
|
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||||
|
playerWrapper.setup(mediaSource, renderer);
|
||||||
|
boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||||
|
playerWrapper.release();
|
||||||
|
assertTrue("Test playback timed out waiting for action schedule to end.", finished);
|
||||||
|
if (playerWrapper.exception != null) {
|
||||||
|
throw playerWrapper.exception;
|
||||||
}
|
}
|
||||||
|
assertEquals(expectedWindowIndices.length, windowIndices.size());
|
||||||
@SafeVarargs
|
for (int i = 0; i < expectedWindowIndices.length; i++) {
|
||||||
public final void assertSourceInfosEquals(Pair<Timeline, Object>... sourceInfos) {
|
assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
|
||||||
assertEquals(sourceInfos.length, this.sourceInfos.size());
|
|
||||||
for (Pair<Timeline, Object> sourceInfo : sourceInfos) {
|
|
||||||
assertEquals(sourceInfo, this.sourceInfos.remove());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
assertEquals(9, playerWrapper.positionDiscontinuityCount);
|
||||||
// ExoPlayer.EventListener implementation.
|
assertTrue(renderer.isEnded);
|
||||||
|
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||||
@Override
|
|
||||||
public void onLoadingChanged(boolean isLoading) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
|
||||||
if (playbackState == ExoPlayer.STATE_ENDED) {
|
|
||||||
endedCountDownLatch.countDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
|
||||||
sourceInfos.add(Pair.create(timeline, manifest));
|
|
||||||
sourceInfoCountDownLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
|
||||||
this.trackGroups = trackGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerError(ExoPlaybackException exception) {
|
|
||||||
handleError(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("NonAtomicVolatileUpdate")
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity() {
|
|
||||||
positionDiscontinuityCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class TimelineWindowDefinition {
|
|
||||||
|
|
||||||
public final boolean isSeekable;
|
|
||||||
public final boolean isDynamic;
|
|
||||||
public final long durationUs;
|
|
||||||
|
|
||||||
public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
|
|
||||||
this.isSeekable = isSeekable;
|
|
||||||
this.isDynamic = isDynamic;
|
|
||||||
this.durationUs = durationUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class FakeTimeline extends Timeline {
|
|
||||||
|
|
||||||
private final TimelineWindowDefinition[] windowDefinitions;
|
|
||||||
|
|
||||||
public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
|
|
||||||
this.windowDefinitions = windowDefinitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getWindowCount() {
|
|
||||||
return windowDefinitions.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
|
||||||
long defaultPositionProjectionUs) {
|
|
||||||
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
|
|
||||||
Object id = setIds ? windowIndex : null;
|
|
||||||
return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable,
|
|
||||||
windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPeriodCount() {
|
|
||||||
return windowDefinitions.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
|
||||||
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
|
|
||||||
Object id = setIds ? periodIndex : null;
|
|
||||||
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getIndexOfPeriod(Object uid) {
|
|
||||||
if (!(uid instanceof Integer)) {
|
|
||||||
return C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
int index = (Integer) uid;
|
|
||||||
return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating
|
|
||||||
* the period will return a {@link FakeMediaPeriod}.
|
|
||||||
*/
|
|
||||||
private static class FakeMediaSource implements MediaSource {
|
|
||||||
|
|
||||||
private final Timeline timeline;
|
|
||||||
private final Object manifest;
|
|
||||||
private final TrackGroupArray trackGroupArray;
|
|
||||||
private final ArrayList<FakeMediaPeriod> activeMediaPeriods;
|
|
||||||
|
|
||||||
private boolean preparedSource;
|
|
||||||
private boolean releasedSource;
|
|
||||||
|
|
||||||
public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) {
|
|
||||||
this.timeline = timeline;
|
|
||||||
this.manifest = manifest;
|
|
||||||
TrackGroup[] trackGroups = new TrackGroup[formats.length];
|
|
||||||
for (int i = 0; i < formats.length; i++) {
|
|
||||||
trackGroups[i] = new TrackGroup(formats[i]);
|
|
||||||
}
|
|
||||||
trackGroupArray = new TrackGroupArray(trackGroups);
|
|
||||||
activeMediaPeriods = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
|
||||||
assertFalse(preparedSource);
|
|
||||||
preparedSource = true;
|
|
||||||
listener.onSourceInfoRefreshed(timeline, manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
|
||||||
assertTrue(preparedSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
|
|
||||||
Assertions.checkIndex(index, 0, timeline.getPeriodCount());
|
|
||||||
assertTrue(preparedSource);
|
|
||||||
assertFalse(releasedSource);
|
|
||||||
assertEquals(0, positionUs);
|
|
||||||
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
|
|
||||||
activeMediaPeriods.add(mediaPeriod);
|
|
||||||
return mediaPeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
|
||||||
assertTrue(preparedSource);
|
|
||||||
assertFalse(releasedSource);
|
|
||||||
FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
|
|
||||||
assertTrue(activeMediaPeriods.remove(fakeMediaPeriod));
|
|
||||||
fakeMediaPeriod.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void releaseSource() {
|
|
||||||
assertTrue(preparedSource);
|
|
||||||
assertFalse(releasedSource);
|
|
||||||
assertTrue(activeMediaPeriods.isEmpty());
|
|
||||||
releasedSource = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that
|
|
||||||
* track will give the player a {@link FakeSampleStream}.
|
|
||||||
*/
|
|
||||||
private static final class FakeMediaPeriod implements MediaPeriod {
|
|
||||||
|
|
||||||
private final TrackGroupArray trackGroupArray;
|
|
||||||
|
|
||||||
private boolean preparedPeriod;
|
|
||||||
|
|
||||||
public FakeMediaPeriod(TrackGroupArray trackGroupArray) {
|
|
||||||
this.trackGroupArray = trackGroupArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void release() {
|
|
||||||
preparedPeriod = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void prepare(Callback callback) {
|
|
||||||
assertFalse(preparedPeriod);
|
|
||||||
preparedPeriod = true;
|
|
||||||
callback.onPrepared(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void maybeThrowPrepareError() throws IOException {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TrackGroupArray getTrackGroups() {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
return trackGroupArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
|
||||||
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
int rendererCount = selections.length;
|
|
||||||
for (int i = 0; i < rendererCount; i++) {
|
|
||||||
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
|
|
||||||
streams[i] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i < rendererCount; i++) {
|
|
||||||
if (streams[i] == null && selections[i] != null) {
|
|
||||||
TrackSelection selection = selections[i];
|
|
||||||
assertEquals(1, selection.length());
|
|
||||||
assertEquals(0, selection.getIndexInTrackGroup(0));
|
|
||||||
TrackGroup trackGroup = selection.getTrackGroup();
|
|
||||||
assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET);
|
|
||||||
streams[i] = new FakeSampleStream(trackGroup.getFormat(0));
|
|
||||||
streamResetFlags[i] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void discardBuffer(long positionUs) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long readDiscontinuity() {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
return C.TIME_UNSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBufferedPositionUs() {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
return C.TIME_END_OF_SOURCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long seekToUs(long positionUs) {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
assertEquals(0, positionUs);
|
|
||||||
return positionUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getNextLoadPositionUs() {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
return C.TIME_END_OF_SOURCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean continueLoading(long positionUs) {
|
|
||||||
assertTrue(preparedPeriod);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag
|
|
||||||
* on its input buffer.
|
|
||||||
*/
|
|
||||||
private static final class FakeSampleStream implements SampleStream {
|
|
||||||
|
|
||||||
private final Format format;
|
|
||||||
|
|
||||||
private boolean readFormat;
|
|
||||||
|
|
||||||
public FakeSampleStream(Format format) {
|
|
||||||
this.format = format;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReady() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
|
||||||
boolean formatRequired) {
|
|
||||||
if (formatRequired || !readFormat) {
|
|
||||||
formatHolder.format = format;
|
|
||||||
readFormat = true;
|
|
||||||
return C.RESULT_FORMAT_READ;
|
|
||||||
} else {
|
|
||||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
|
||||||
return C.RESULT_BUFFER_READ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void maybeThrowError() throws IOException {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void skipData(long positionUs) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fake {@link Renderer} that supports any format with the matching MIME type. The renderer
|
|
||||||
* verifies that it reads a given {@link Format}.
|
|
||||||
*/
|
|
||||||
private static class FakeRenderer extends BaseRenderer {
|
|
||||||
|
|
||||||
private final Format expectedFormat;
|
|
||||||
|
|
||||||
public int positionResetCount;
|
|
||||||
public int formatReadCount;
|
|
||||||
public int bufferReadCount;
|
|
||||||
public boolean isEnded;
|
|
||||||
|
|
||||||
public FakeRenderer(Format expectedFormat) {
|
|
||||||
super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN
|
|
||||||
: MimeTypes.getTrackType(expectedFormat.sampleMimeType));
|
|
||||||
this.expectedFormat = expectedFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
|
||||||
positionResetCount++;
|
|
||||||
isEnded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
|
||||||
if (isEnded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the format matches the expected format.
|
|
||||||
FormatHolder formatHolder = new FormatHolder();
|
|
||||||
DecoderInputBuffer buffer =
|
|
||||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
|
||||||
int result = readSource(formatHolder, buffer, false);
|
|
||||||
if (result == C.RESULT_FORMAT_READ) {
|
|
||||||
formatReadCount++;
|
|
||||||
assertEquals(expectedFormat, formatHolder.format);
|
|
||||||
} else if (result == C.RESULT_BUFFER_READ) {
|
|
||||||
bufferReadCount++;
|
|
||||||
if (buffer.isEndOfStream()) {
|
|
||||||
isEnded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReady() {
|
|
||||||
return isSourceReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnded() {
|
|
||||||
return isEnded;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int supportsFormat(Format format) throws ExoPlaybackException {
|
|
||||||
return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED
|
|
||||||
: FORMAT_UNSUPPORTED_TYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {
|
|
||||||
|
|
||||||
public FakeMediaClockRenderer(Format expectedFormat) {
|
|
||||||
super(expectedFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MediaClock getMediaClock() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,9 @@ public final class FormatTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void testParcelable() {
|
public void testParcelable() {
|
||||||
DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4,
|
DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4,
|
||||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||||
DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM,
|
DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM,
|
||||||
TestUtil.buildTestData(128, 1 /* data seed */));
|
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||||
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
|
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
|
||||||
byte[] projectionData = new byte[] {1, 2, 3};
|
byte[] projectionData = new byte[] {1, 2, 3};
|
||||||
|
@ -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 {
|
public class DrmInitDataTest extends TestCase {
|
||||||
|
|
||||||
private static final SchemeData DATA_1 =
|
private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
|
||||||
new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
|
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||||
private static final SchemeData DATA_2 =
|
private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
|
||||||
new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
|
TestUtil.buildTestData(128, 2 /* data seed */));
|
||||||
private static final SchemeData DATA_1B =
|
private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4,
|
||||||
new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */));
|
TestUtil.buildTestData(128, 1 /* data seed */));
|
||||||
private static final SchemeData DATA_2B =
|
private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4,
|
||||||
new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */));
|
TestUtil.buildTestData(128, 2 /* data seed */));
|
||||||
private static final SchemeData DATA_UNIVERSAL =
|
private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4,
|
||||||
new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */));
|
TestUtil.buildTestData(128, 3 /* data seed */));
|
||||||
|
|
||||||
public void testParcelable() {
|
public void testParcelable() {
|
||||||
DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);
|
DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2);
|
||||||
|
@ -154,7 +154,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static DrmInitData newDrmInitData() {
|
private static DrmInitData newDrmInitData() {
|
||||||
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
|
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType",
|
||||||
new byte[] {1, 4, 7, 0, 3, 6}));
|
new byte[] {1, 4, 7, 0, 3, 6}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.flv;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link FlvExtractor}.
|
* Unit test for {@link FlvExtractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class FlvExtractorTest extends InstrumentationTestCase {
|
public final class FlvExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new FlvExtractor();
|
return new FlvExtractor();
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mkv;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link MatroskaExtractor}.
|
* Tests for {@link MatroskaExtractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class MatroskaExtractorTest extends InstrumentationTestCase {
|
public final class MatroskaExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testMkvSample() throws Exception {
|
public void testMkvSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new MatroskaExtractor();
|
return new MatroskaExtractor();
|
||||||
@ -34,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void testWebmSubsampleEncryption() throws Exception {
|
public void testWebmSubsampleEncryption() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new MatroskaExtractor();
|
return new MatroskaExtractor();
|
||||||
@ -43,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception {
|
public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new MatroskaExtractor();
|
return new MatroskaExtractor();
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mp3;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link Mp3Extractor}.
|
* Unit test for {@link Mp3Extractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class Mp3ExtractorTest extends InstrumentationTestCase {
|
public final class Mp3ExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testMp3Sample() throws Exception {
|
public void testMp3Sample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new Mp3Extractor();
|
return new Mp3Extractor();
|
||||||
@ -34,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void testTrimmedMp3Sample() throws Exception {
|
public void testTrimmedMp3Sample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new Mp3Extractor();
|
return new Mp3Extractor();
|
||||||
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
|
|||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link FragmentedMp4Extractor}.
|
* Unit test for {@link FragmentedMp4Extractor}.
|
||||||
@ -26,26 +27,28 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
|
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
|
ExtractorAsserts
|
||||||
|
.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSampleWithSeiPayloadParsing() throws Exception {
|
public void testSampleWithSeiPayloadParsing() throws Exception {
|
||||||
// Enabling the CEA-608 track enables SEI payload parsing.
|
// Enabling the CEA-608 track enables SEI payload parsing.
|
||||||
TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
|
ExtractorAsserts.assertOutput(
|
||||||
|
getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
|
||||||
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
|
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testAtomWithZeroSize() throws Exception {
|
public void testAtomWithZeroSize() throws Exception {
|
||||||
TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
|
ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
|
||||||
getInstrumentation(), ParserException.class);
|
getInstrumentation(), ParserException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestUtil.ExtractorFactory getExtractorFactory() {
|
private static ExtractorFactory getExtractorFactory() {
|
||||||
return getExtractorFactory(0);
|
return getExtractorFactory(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) {
|
private static ExtractorFactory getExtractorFactory(final int flags) {
|
||||||
return new TestUtil.ExtractorFactory() {
|
return new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new FragmentedMp4Extractor(flags, null);
|
return new FragmentedMp4Extractor(flags, null);
|
||||||
|
@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4;
|
|||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link Mp4Extractor}.
|
* Tests for {@link Mp4Extractor}.
|
||||||
@ -27,7 +28,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class Mp4ExtractorTest extends InstrumentationTestCase {
|
public final class Mp4ExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testMp4Sample() throws Exception {
|
public void testMp4Sample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new Mp4Extractor();
|
return new Mp4Extractor();
|
||||||
|
@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ogg;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil.ExtractorFactory;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,20 +36,21 @@ public final class OggExtractorTest extends InstrumentationTestCase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public void testOpus() throws Exception {
|
public void testOpus() throws Exception {
|
||||||
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation());
|
ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testFlac() throws Exception {
|
public void testFlac() throws Exception {
|
||||||
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation());
|
ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testFlacNoSeektable() throws Exception {
|
public void testFlacNoSeektable() throws Exception {
|
||||||
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
|
ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg",
|
||||||
getInstrumentation());
|
getInstrumentation());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testVorbis() throws Exception {
|
public void testVorbis() throws Exception {
|
||||||
TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation());
|
ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg",
|
||||||
|
getInstrumentation());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSniffVorbis() throws Exception {
|
public void testSniffVorbis() throws Exception {
|
||||||
|
@ -19,7 +19,8 @@ import android.annotation.TargetApi;
|
|||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,8 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
|||||||
public final class RawCcExtractorTest extends InstrumentationTestCase {
|
public final class RawCcExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testRawCcSample() throws Exception {
|
public void testRawCcSample() throws Exception {
|
||||||
TestUtil.assertOutput(
|
ExtractorAsserts.assertOutput(
|
||||||
new TestUtil.ExtractorFactory() {
|
new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new RawCcExtractor(
|
return new RawCcExtractor(
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link Ac3Extractor}.
|
* Unit test for {@link Ac3Extractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class Ac3ExtractorTest extends InstrumentationTestCase {
|
public final class Ac3ExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new Ac3Extractor();
|
return new Ac3Extractor();
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link AdtsExtractor}.
|
* Unit test for {@link AdtsExtractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class AdtsExtractorTest extends InstrumentationTestCase {
|
public final class AdtsExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new AdtsExtractor();
|
return new AdtsExtractor();
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link PsExtractor}.
|
* Unit test for {@link PsExtractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class PsExtractorTest extends InstrumentationTestCase {
|
public final class PsExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new PsExtractor();
|
return new PsExtractor();
|
||||||
|
@ -25,6 +25,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder;
|
|||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
||||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||||
@ -43,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||||||
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
|
private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new TsExtractor();
|
return new TsExtractor();
|
||||||
@ -65,7 +67,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||||||
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
|
writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1);
|
||||||
fileData = out.toByteArray();
|
fileData = out.toByteArray();
|
||||||
|
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new TsExtractor();
|
return new TsExtractor();
|
||||||
@ -75,7 +77,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||||||
|
|
||||||
public void testCustomPesReader() throws Exception {
|
public void testCustomPesReader() throws Exception {
|
||||||
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
|
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
|
||||||
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
|
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
|
||||||
factory);
|
factory);
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
|
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
|
||||||
@ -100,7 +102,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||||||
|
|
||||||
public void testCustomInitialSectionReader() throws Exception {
|
public void testCustomInitialSectionReader() throws Exception {
|
||||||
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
|
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
|
||||||
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
|
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0),
|
||||||
factory);
|
factory);
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
|
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
|
||||||
|
@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.wav;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link WavExtractor}.
|
* Unit test for {@link WavExtractor}.
|
||||||
@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||||||
public final class WavExtractorTest extends InstrumentationTestCase {
|
public final class WavExtractorTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSample() throws Exception {
|
public void testSample() throws Exception {
|
||||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
ExtractorAsserts.assertOutput(new ExtractorFactory() {
|
||||||
@Override
|
@Override
|
||||||
public Extractor create() {
|
public Extractor create() {
|
||||||
return new WavExtractor();
|
return new WavExtractor();
|
||||||
|
@ -15,20 +15,17 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source;
|
package com.google.android.exoplayer2.source;
|
||||||
|
|
||||||
import static org.mockito.Mockito.doAnswer;
|
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.Timeline.Period;
|
import com.google.android.exoplayer2.Timeline.Period;
|
||||||
import com.google.android.exoplayer2.Timeline.Window;
|
import com.google.android.exoplayer2.Timeline.Window;
|
||||||
import com.google.android.exoplayer2.source.MediaSource.Listener;
|
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import org.mockito.Mock;
|
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||||
import org.mockito.Mockito;
|
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
|
||||||
import org.mockito.stubbing.Answer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link ClippingMediaSource}.
|
* Unit tests for {@link ClippingMediaSource}.
|
||||||
@ -38,15 +35,11 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
|
|||||||
private static final long TEST_PERIOD_DURATION_US = 1000000;
|
private static final long TEST_PERIOD_DURATION_US = 1000000;
|
||||||
private static final long TEST_CLIP_AMOUNT_US = 300000;
|
private static final long TEST_CLIP_AMOUNT_US = 300000;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private MediaSource mockMediaSource;
|
|
||||||
private Timeline clippedTimeline;
|
|
||||||
private Window window;
|
private Window window;
|
||||||
private Period period;
|
private Period period;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
TestUtil.setUpMockito(this);
|
|
||||||
window = new Timeline.Window();
|
window = new Timeline.Window();
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
}
|
}
|
||||||
@ -109,35 +102,30 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase {
|
|||||||
clippedTimeline.getPeriod(0, period).getDurationUs());
|
clippedTimeline.getPeriod(0, period).getDurationUs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testWindowAndPeriodIndices() {
|
||||||
|
Timeline timeline = new FakeTimeline(
|
||||||
|
new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
|
||||||
|
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
|
||||||
|
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
|
||||||
|
TimelineAsserts.assertWindowIds(clippedTimeline, 111);
|
||||||
|
TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);
|
||||||
|
TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF,
|
||||||
|
C.INDEX_UNSET);
|
||||||
|
TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0);
|
||||||
|
TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0);
|
||||||
|
TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF,
|
||||||
|
C.INDEX_UNSET);
|
||||||
|
TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0);
|
||||||
|
TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
|
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
|
||||||
*/
|
*/
|
||||||
private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
|
private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
|
||||||
mockMediaSourceSourceWithTimeline(timeline);
|
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||||
new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
|
return TestUtil.extractTimelineFromMediaSource(
|
||||||
new Listener() {
|
new ClippingMediaSource(mediaSource, startMs, endMs));
|
||||||
@Override
|
|
||||||
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
|
|
||||||
clippedTimeline = timeline;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return clippedTimeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
|
|
||||||
*/
|
|
||||||
private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
|
|
||||||
doAnswer(new Answer<Void>() {
|
|
||||||
@Override
|
|
||||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
|
||||||
MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
|
|
||||||
listener.onSourceInfoRefreshed(timeline, null);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
|
|
||||||
Mockito.any(MediaSource.Listener.class));
|
|
||||||
return mockMediaSource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
assertEquals(2, output.size());
|
||||||
Cue ttmlCue = output.get(0);
|
Cue ttmlCue = output.get(0);
|
||||||
assertEquals("lorem", ttmlCue.text.toString());
|
assertEquals("lorem", ttmlCue.text.toString());
|
||||||
assertEquals(10.f / 100.f, ttmlCue.position);
|
assertEquals(10f / 100f, ttmlCue.position);
|
||||||
assertEquals(10.f / 100.f, ttmlCue.line);
|
assertEquals(10f / 100f, ttmlCue.line);
|
||||||
assertEquals(20.f / 100.f, ttmlCue.size);
|
assertEquals(20f / 100f, ttmlCue.size);
|
||||||
|
|
||||||
ttmlCue = output.get(1);
|
ttmlCue = output.get(1);
|
||||||
assertEquals("amet", ttmlCue.text.toString());
|
assertEquals("amet", ttmlCue.text.toString());
|
||||||
assertEquals(60.f / 100.f, ttmlCue.position);
|
assertEquals(60f / 100f, ttmlCue.position);
|
||||||
assertEquals(10.f / 100.f, ttmlCue.line);
|
assertEquals(10f / 100f, ttmlCue.line);
|
||||||
assertEquals(20.f / 100.f, ttmlCue.size);
|
assertEquals(20f / 100f, ttmlCue.size);
|
||||||
|
|
||||||
output = subtitle.getCues(5000000);
|
output = subtitle.getCues(5000000);
|
||||||
assertEquals(1, output.size());
|
assertEquals(1, output.size());
|
||||||
ttmlCue = output.get(0);
|
ttmlCue = output.get(0);
|
||||||
assertEquals("ipsum", ttmlCue.text.toString());
|
assertEquals("ipsum", ttmlCue.text.toString());
|
||||||
assertEquals(40.f / 100.f, ttmlCue.position);
|
assertEquals(40f / 100f, ttmlCue.position);
|
||||||
assertEquals(40.f / 100.f, ttmlCue.line);
|
assertEquals(40f / 100f, ttmlCue.line);
|
||||||
assertEquals(20.f / 100.f, ttmlCue.size);
|
assertEquals(20f / 100f, ttmlCue.size);
|
||||||
|
|
||||||
output = subtitle.getCues(9000000);
|
output = subtitle.getCues(9000000);
|
||||||
assertEquals(1, output.size());
|
assertEquals(1, output.size());
|
||||||
ttmlCue = output.get(0);
|
ttmlCue = output.get(0);
|
||||||
assertEquals("dolor", ttmlCue.text.toString());
|
assertEquals("dolor", ttmlCue.text.toString());
|
||||||
assertEquals(10.f / 100.f, ttmlCue.position);
|
assertEquals(Cue.DIMEN_UNSET, ttmlCue.position);
|
||||||
assertEquals(80.f / 100.f, ttmlCue.line);
|
assertEquals(Cue.DIMEN_UNSET, ttmlCue.line);
|
||||||
assertEquals(Cue.DIMEN_UNSET, ttmlCue.size);
|
assertEquals(Cue.DIMEN_UNSET, ttmlCue.size);
|
||||||
|
// TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
|
||||||
|
// assertEquals(10f / 100f, ttmlCue.position);
|
||||||
|
// assertEquals(80f / 100f, ttmlCue.line);
|
||||||
|
// assertEquals(1f, ttmlCue.size);
|
||||||
|
|
||||||
output = subtitle.getCues(21000000);
|
output = subtitle.getCues(21000000);
|
||||||
assertEquals(1, output.size());
|
assertEquals(1, output.size());
|
||||||
ttmlCue = output.get(0);
|
ttmlCue = output.get(0);
|
||||||
assertEquals("She first said this", ttmlCue.text.toString());
|
assertEquals("She first said this", ttmlCue.text.toString());
|
||||||
assertEquals(45.f / 100.f, ttmlCue.position);
|
assertEquals(45f / 100f, ttmlCue.position);
|
||||||
assertEquals(45.f / 100.f, ttmlCue.line);
|
assertEquals(45f / 100f, ttmlCue.line);
|
||||||
assertEquals(35.f / 100.f, ttmlCue.size);
|
assertEquals(35f / 100f, ttmlCue.size);
|
||||||
output = subtitle.getCues(25000000);
|
output = subtitle.getCues(25000000);
|
||||||
ttmlCue = output.get(0);
|
ttmlCue = output.get(0);
|
||||||
assertEquals("She first said this\nThen this", ttmlCue.text.toString());
|
assertEquals("She first said this\nThen this", ttmlCue.text.toString());
|
||||||
@ -197,8 +201,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase {
|
|||||||
assertEquals(1, output.size());
|
assertEquals(1, output.size());
|
||||||
ttmlCue = output.get(0);
|
ttmlCue = output.get(0);
|
||||||
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
|
assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString());
|
||||||
assertEquals(45.f / 100.f, ttmlCue.position);
|
assertEquals(45f / 100f, ttmlCue.position);
|
||||||
assertEquals(45.f / 100.f, ttmlCue.line);
|
assertEquals(45f / 100f, ttmlCue.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {
|
public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException {
|
||||||
|
@ -125,25 +125,25 @@ public final class CssParserTest extends InstrumentationTestCase {
|
|||||||
String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n";
|
String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n";
|
||||||
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));
|
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput));
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "lorem");
|
assertEquals("lorem", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), ":");
|
assertEquals(":", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
|
assertEquals("ipsum", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "{");
|
assertEquals("{", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "dolor");
|
assertEquals("dolor", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "}");
|
assertEquals("}", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "#sit");
|
assertEquals("#sit", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), ",");
|
assertEquals(",", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "amet");
|
assertEquals("amet", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), ";");
|
assertEquals(";", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "lorem");
|
assertEquals("lorem", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), ":");
|
assertEquals(":", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "ipsum");
|
assertEquals("ipsum", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "dolor");
|
assertEquals("dolor", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "(");
|
assertEquals("(", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), "(");
|
assertEquals("(", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), ")");
|
assertEquals(")", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), ")");
|
assertEquals(")", CssParser.parseNextToken(input, builder));
|
||||||
assertEquals(CssParser.parseNextToken(input, builder), null);
|
assertEquals(null, CssParser.parseNextToken(input, builder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testStyleScoreSystem() {
|
public void testStyleScoreSystem() {
|
||||||
|
@ -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;
|
package com.google.android.exoplayer2.upstream.cache;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import android.test.MoreAsserts;
|
import android.test.MoreAsserts;
|
||||||
@ -38,27 +40,29 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||||||
private static final String KEY_1 = "key 1";
|
private static final String KEY_1 = "key 1";
|
||||||
private static final String KEY_2 = "key 2";
|
private static final String KEY_2 = "key 2";
|
||||||
|
|
||||||
private File cacheDir;
|
private File tempFolder;
|
||||||
private SimpleCache simpleCache;
|
private SimpleCache cache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
super.setUp();
|
||||||
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||||
|
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void tearDown() throws Exception {
|
public void tearDown() throws Exception {
|
||||||
Util.recursiveDelete(cacheDir);
|
Util.recursiveDelete(tempFolder);
|
||||||
|
super.tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testMaxCacheFileSize() throws Exception {
|
public void testMaxCacheFileSize() throws Exception {
|
||||||
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
|
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
|
||||||
assertReadDataContentLength(cacheDataSource, false, false);
|
assertReadDataContentLength(cacheDataSource, false, false);
|
||||||
File[] files = cacheDir.listFiles();
|
for (String key : cache.getKeys()) {
|
||||||
for (File file : files) {
|
for (CacheSpan cacheSpan : cache.getCachedSpans(key)) {
|
||||||
if (!file.getName().equals(CachedContentIndex.FILE_NAME)) {
|
assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE);
|
||||||
assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
|
assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,7 +108,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||||||
// Read partial at EOS but don't cross it so length is unknown
|
// Read partial at EOS but don't cross it so length is unknown
|
||||||
CacheDataSource cacheDataSource = createCacheDataSource(false, true);
|
CacheDataSource cacheDataSource = createCacheDataSource(false, true);
|
||||||
assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2);
|
assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2);
|
||||||
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
|
assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1));
|
||||||
|
|
||||||
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
|
// Now do an unbounded request for whole data. This will cause a bounded request from upstream.
|
||||||
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
|
// End of data from upstream shouldn't be mixed up with EOS and cause length set wrong.
|
||||||
@ -124,13 +128,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||||||
CacheDataSource cacheDataSource = createCacheDataSource(false, true,
|
CacheDataSource cacheDataSource = createCacheDataSource(false, true,
|
||||||
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
|
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
|
||||||
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
|
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
|
||||||
MoreAsserts.assertEmpty(simpleCache.getKeys());
|
MoreAsserts.assertEmpty(cache.getKeys());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testReadOnlyCache() throws Exception {
|
public void testReadOnlyCache() throws Exception {
|
||||||
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
|
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
|
||||||
assertReadDataContentLength(cacheDataSource, false, false);
|
assertReadDataContentLength(cacheDataSource, false, false);
|
||||||
assertEquals(0, cacheDir.list().length);
|
assertCacheEmpty(cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
|
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
|
||||||
@ -155,30 +159,30 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||||||
assertReadData(cacheDataSource, unknownLength, 0, length);
|
assertReadData(cacheDataSource, unknownLength, 0, length);
|
||||||
assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache "
|
assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache "
|
||||||
+ "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length,
|
+ "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length,
|
||||||
simpleCache.getContentLength(KEY_1));
|
cache.getContentLength(KEY_1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position,
|
private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position,
|
||||||
int length) throws IOException {
|
int length) throws IOException {
|
||||||
int actualLength = TEST_DATA.length - position;
|
int testDataLength = TEST_DATA.length - position;
|
||||||
if (length != C.LENGTH_UNSET) {
|
if (length != C.LENGTH_UNSET) {
|
||||||
actualLength = Math.min(actualLength, length);
|
testDataLength = Math.min(testDataLength, length);
|
||||||
}
|
}
|
||||||
assertEquals(unknownLength ? length : actualLength,
|
assertEquals(unknownLength ? length : testDataLength,
|
||||||
cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1)));
|
cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1)));
|
||||||
|
|
||||||
byte[] buffer = new byte[100];
|
byte[] buffer = new byte[100];
|
||||||
int index = 0;
|
int totalBytesRead = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
int read = cacheDataSource.read(buffer, index, buffer.length - index);
|
int read = cacheDataSource.read(buffer, totalBytesRead, buffer.length - totalBytesRead);
|
||||||
if (read == C.RESULT_END_OF_INPUT) {
|
if (read == C.RESULT_END_OF_INPUT) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
index += read;
|
totalBytesRead += read;
|
||||||
}
|
}
|
||||||
assertEquals(actualLength, index);
|
assertEquals(testDataLength, totalBytesRead);
|
||||||
MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength),
|
MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength),
|
||||||
Arrays.copyOf(buffer, index));
|
Arrays.copyOf(buffer, totalBytesRead));
|
||||||
|
|
||||||
cacheDataSource.close();
|
cacheDataSource.close();
|
||||||
}
|
}
|
||||||
@ -192,7 +196,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||||
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
|
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
|
||||||
return createCacheDataSource(setReadException, simulateUnknownLength, flags,
|
return createCacheDataSource(setReadException, simulateUnknownLength, flags,
|
||||||
new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE));
|
new CacheDataSink(cache, MAX_CACHE_FILE_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||||
@ -204,7 +208,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||||||
if (setReadException) {
|
if (setReadException) {
|
||||||
fakeData.appendReadError(new IOException("Shouldn't read from upstream"));
|
fakeData.appendReadError(new IOException("Shouldn't read from upstream"));
|
||||||
}
|
}
|
||||||
return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
|
return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink,
|
||||||
flags, null);
|
flags, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user