diff --git a/README.md b/README.md index 3de86d21a3..d7bc23f700 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates. ## Using ExoPlayer ## +ExoPlayer modules can be obtained via jCenter. It's also possible to clone the +repository and depend on the modules locally. + +### Via jCenter ### + The easiest way to get started using ExoPlayer is to add it as a gradle dependency. You need to make sure you have the jcenter repository included in the `build.gradle` file in the root of your project: @@ -64,6 +69,39 @@ latest versions, see the [Release notes][]. [Bintray]: https://bintray.com/google/exoplayer [Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md +### Locally ### + +Cloning the repository and depending on the modules locally is required when +using some ExoPlayer extension modules. It's also a suitable approach if you +want to make local changes to ExoPlayer, or if you want to use a development +branch. + +First, clone the repository into a local directory and checkout the desired +branch: + +```sh +git clone https://github.com/google/ExoPlayer.git +git checkout release-v2 +``` + +Next, add the following to your project's `settings.gradle` file, replacing +`path/to/exoplayer` with the path to your local copy: + +```gradle +gradle.ext.exoplayerRoot = 'path/to/exoplayer' +gradle.ext.exoplayerModulePrefix = 'exoplayer-' +apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle') +``` + +You should now see the ExoPlayer modules appear as part of your project. You can +depend on them as you would on any other local module, for example: + +```gradle +compile project(':exoplayer-library-core') +compile project(':exoplayer-library-dash') +compile project(':exoplayer-library-ui) +``` + ## Developing ExoPlayer ## #### Project branches #### diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ff1bd42fde..4101caad47 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,59 @@ # Release notes # +### r2.5.0 ### + +* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an + easy and seamless way of incorporating display ads into ExoPlayer playbacks. + You can read more about the IMA extension + [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). +* MediaSession extension: Provides an easy to to connect ExoPlayer with + MediaSessionCompat in the Android Support Library. +* RTMP extension: An extension for playing streams over RTMP. +* Build: Made it easier for application developers to depend on a local checkout + of ExoPlayer. You can learn how to do this + [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720). +* Core playback improvements: + * Eliminated re-buffering when changing audio and text track selections during + playback of progressive streams + ([#2926](https://github.com/google/ExoPlayer/issues/2926)). + * New DynamicConcatenatingMediaSource class to support playback of dynamic + playlists. + * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode + during playback. Use of setRepeatMode should be preferred to + LoopingMediaSource for most looping use cases. You can read more about + setRepeatMode + [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3). + * Eliminated jank when switching video playback from one Surface to another on + API level 23+ for unencrypted content, and on devices that support the + EGL_EXT_protected_content OpenGL extension for protected content + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Enabled ExoPlayer instantiation on background threads without Loopers. + Events from such players are delivered on the application's main thread. +* HLS improvements: + * Optimized adaptive switches for playlists that specify the + EXT-X-INDEPENDENT-SEGMENTS tag. + * Optimized in-buffer seeking + ([#551](https://github.com/google/ExoPlayer/issues/551)). + * Eliminated re-buffering when changing audio and text track selections during + playback, provided the new selection does not require switching to different + renditions ([#2718](https://github.com/google/ExoPlayer/issues/2718)). + * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object. +* DASH: Support for seamless switching across streams in different AdaptationSet + elements ([#2431](https://github.com/google/ExoPlayer/issues/2431)). +* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on + API level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)). +* Captions: Initial support for SSA/ASS subtitles + ([#889](https://github.com/google/ExoPlayer/issues/889)). +* AndroidTV: Fixed issue where tunneled video playback would not start on some + devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)). +* MPEG-TS: Fixed segmentation issue when parsing H262 + ([#2891](https://github.com/google/ExoPlayer/issues/2891)). +* Cronet extension: Support for a user-defined fallback if Cronet library is not + present. +* Fix buffer too small IllegalStateException issue affecting some composite + media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). +* Misc bugfixes. + ### r2.4.4 ### * HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance diff --git a/build.gradle b/build.gradle index a4ae1f175e..dbc8a41eb0 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,8 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.1' - classpath 'com.novoda:bintray-release:0.4.0' + classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: // https://code.google.com/p/android/issues/detail?id=226070 @@ -31,25 +31,12 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + } } project.ext { - // Important: ExoPlayer specifies a minSdkVersion of 9 because various - // components provided by the library may be of use on older devices. - // However, please note that the core media playback functionality - // provided by the library requires API level 16 or greater. - minSdkVersion = 9 - compileSdkVersion = 25 - targetSdkVersion = 25 - buildToolsVersion = '25' - testSupportLibraryVersion = '0.5' - supportLibraryVersion = '25.3.1' - dexmakerVersion = '1.2' - mockitoVersion = '1.9.5' - releaseRepoName = getBintrayRepo() - releaseUserOrg = 'google' - releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.4' - releaseWebsite = 'https://github.com/google/ExoPlayer' + exoplayerPublishEnabled = true } if (it.hasProperty('externalBuildDir')) { if (!new File(externalBuildDir).isAbsolute()) { @@ -59,10 +46,4 @@ allprojects { } } -def getBintrayRepo() { - boolean publicRepo = hasProperty('publicRepo') && - property('publicRepo').toBoolean() - return publicRepo ? 'exoplayer' : 'exoplayer-test' -} - apply from: 'javadoc_combined.gradle' diff --git a/constants.gradle b/constants.gradle new file mode 100644 index 0000000000..7d126ccd89 --- /dev/null +++ b/constants.gradle @@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +project.ext { + // Important: ExoPlayer specifies a minSdkVersion of 9 because various + // components provided by the library may be of use on older devices. + // However, please note that the core media playback functionality provided + // by the library requires API level 16 or greater. + minSdkVersion = 9 + compileSdkVersion = 25 + targetSdkVersion = 25 + buildToolsVersion = '25' + testSupportLibraryVersion = '0.5' + supportLibraryVersion = '25.4.0' + dexmakerVersion = '1.2' + mockitoVersion = '1.9.5' + releaseVersion = 'r2.5.0' + modulePrefix = ':' + if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix + } +} diff --git a/core_settings.gradle b/core_settings.gradle new file mode 100644 index 0000000000..20e7b235a2 --- /dev/null +++ b/core_settings.gradle @@ -0,0 +1,58 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +def rootDir = gradle.ext.exoplayerRoot +def modulePrefix = ':' +if (gradle.ext.has('exoplayerModulePrefix')) { + modulePrefix += gradle.ext.exoplayerModulePrefix +} + +include modulePrefix + 'library' +include modulePrefix + 'library-core' +include modulePrefix + 'library-dash' +include modulePrefix + 'library-hls' +include modulePrefix + 'library-smoothstreaming' +include modulePrefix + 'library-ui' +include modulePrefix + 'testutils' +include modulePrefix + 'extension-ffmpeg' +include modulePrefix + 'extension-flac' +include modulePrefix + 'extension-gvr' +include modulePrefix + 'extension-ima' +include modulePrefix + 'extension-mediasession' +include modulePrefix + 'extension-okhttp' +include modulePrefix + 'extension-opus' +include modulePrefix + 'extension-vp9' +include modulePrefix + 'extension-rtmp' + +project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') +project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') +project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash') +project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls') +project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') +project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') +project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') +project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') +project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') +project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') +project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') +project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') +project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') +project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') +project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') +project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') + +if (gradle.ext.has('exoplayerIncludeCronetExtension') + && gradle.ext.exoplayerIncludeCronetExtension) { + include modulePrefix + 'extension-cronet' + project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') +} diff --git a/demo/build.gradle b/demo/build.gradle index be5e52a25c..7eea25478f 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../constants.gradle' apply plugin: 'com.android.application' android { @@ -45,13 +46,14 @@ android { } dependencies { - compile project(':library-core') - compile project(':library-dash') - compile project(':library-hls') - compile project(':library-smoothstreaming') - compile project(':library-ui') - withExtensionsCompile project(path: ':extension-ffmpeg') - withExtensionsCompile project(path: ':extension-flac') - withExtensionsCompile project(path: ':extension-opus') - withExtensionsCompile project(path: ':extension-vp9') + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') + compile project(modulePrefix + 'library-ui') + withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg') + withExtensionsCompile project(path: modulePrefix + 'extension-flac') + withExtensionsCompile project(path: modulePrefix + 'extension-ima') + withExtensionsCompile project(path: modulePrefix + 'extension-opus') + withExtensionsCompile project(path: modulePrefix + 'extension-vp9') } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index afcddccac9..0e04d9a435 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,14 +16,14 @@ + android:versionCode="2500" + android:versionName="2.5.0"> - + Dogs", "playlist": [ { - "uri": "http://html5demos.com/assets/dizzy.mp4" + "uri": "https://html5demos.com/assets/dizzy.mp4" }, { - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" } ] }, @@ -422,7 +458,7 @@ "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" }, { - "uri": "http://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" }, { "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" @@ -435,13 +471,13 @@ "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test", "playlist": [ { - "uri": "http://html5demos.com/assets/dizzy.mp4" + "uri": "https://html5demos.com/assets/dizzy.mp4" }, { "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" }, { - "uri": "http://html5demos.com/assets/dizzy.mp4" + "uri": "https://html5demos.com/assets/dizzy.mp4" }, { "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" @@ -452,5 +488,85 @@ ] } ] + }, + { + "name": "IMA sample ad tags", + "samples": [ + { + "name": "Single inline linear", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + }, + { + "name": "Single skippable inline", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dskippablelinear&correlator=" + }, + { + "name": "Single redirect linear", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirectlinear&correlator=" + }, + { + "name": "Single redirect error", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&nofb=1&correlator=" + }, + { + "name": "Single redirect broken (fallback)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dredirecterror&correlator=" + }, + { + "name": "VMAP pre-roll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonly&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll + bumper", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpreonlybumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP post-roll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonly&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP post-roll + bumper", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpostonlybumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-, mid- and post-rolls, single ads", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpod&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpod&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostpodbumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll optimized pod with 3 ads, post-roll single ad (bumpers around all ad breaks)", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostoptimizedpodbumper&cmsid=496&vid=short_onecue&correlator=" + }, + { + "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" + } + ] } ] diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 953021fe6f..30dfb5140a 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -20,9 +20,9 @@ import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -55,8 +55,8 @@ import java.util.Locale; /** * Logs player events using {@link Log}. */ -/* package */ final class EventLogger implements ExoPlayer.EventListener, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, +/* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener, + VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener, MetadataRenderer.Output { @@ -82,7 +82,7 @@ import java.util.Locale; startTimeMs = SystemClock.elapsedRealtime(); } - // ExoPlayer.EventListener + // Player.EventListener @Override public void onLoadingChanged(boolean isLoading) { @@ -95,6 +95,11 @@ import java.util.Locale; + getStateString(state) + "]"); } + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); + } + @Override public void onPositionDiscontinuity() { Log.d(TAG, "positionDiscontinuity"); @@ -276,7 +281,7 @@ import java.util.Locale; @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - // Do nothing. + Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]"); } @Override @@ -407,13 +412,13 @@ import java.util.Locale; private static String getStateString(int state) { switch (state) { - case ExoPlayer.STATE_BUFFERING: + case Player.STATE_BUFFERING: return "B"; - case ExoPlayer.STATE_ENDED: + case Player.STATE_ENDED: return "E"; - case ExoPlayer.STATE_IDLE: + case Player.STATE_IDLE: return "I"; - case ExoPlayer.STATE_READY: + case Player.STATE_READY: return "R"; default: return "?"; @@ -461,4 +466,16 @@ import java.util.Locale; return enabled ? "[X]" : "[ ]"; } + private static String getRepeatModeString(@Player.RepeatMode int repeatMode) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return "OFF"; + case Player.REPEAT_MODE_ONE: + return "ONE"; + case Player.REPEAT_MODE_ALL: + return "ALL"; + default: + return "?"; + } + } } diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index d0703f3496..9e53dff857 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.demo; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -26,16 +27,19 @@ import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; +import android.view.ViewGroup; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; @@ -69,6 +73,8 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; @@ -77,7 +83,7 @@ import java.util.UUID; /** * An activity that plays media using {@link SimpleExoPlayer}. */ -public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener, +public class PlayerActivity extends Activity implements OnClickListener, EventListener, PlaybackControlView.VisibilityListener { public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; @@ -92,6 +98,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay "com.google.android.exoplayer.demo.action.VIEW_LIST"; public static final String URI_LIST_EXTRA = "uri_list"; public static final String EXTENSION_LIST_EXTRA = "extension_list"; + public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; @@ -112,13 +119,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private DefaultTrackSelector trackSelector; private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; - private boolean needRetrySource; + private boolean inErrorState; private TrackGroupArray lastSeenTrackGroupArray; private boolean shouldAutoPlay; private int resumeWindow; private long resumePosition; + // Fields used only for ad playback. The ads loader is loaded via reflection. + + private Object imaAdsLoader; // com.google.android.exoplayer2.ext.ima.ImaAdsLoader + private Uri loadedAdTagUri; + private ViewGroup adOverlayViewGroup; + // Activity lifecycle @Override @@ -185,6 +198,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } + @Override + public void onDestroy() { + super.onDestroy(); + releaseAdsLoader(); + } + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { @@ -200,10 +219,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public boolean dispatchKeyEvent(KeyEvent event) { - // Show the controls on any key event. - simpleExoPlayerView.showController(); - // If the event was not handled then see if the player view can handle it as a media key event. - return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event); + // If the event was not handled then see if the player view can handle it. + return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event); } // OnClickListener methods @@ -247,13 +264,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - try { - drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + int errorStringId = R.string.error_drm_unknown; + if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_not_supported; + } else { + try { + drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, + keyRequestPropertiesArray); + } catch (UnsupportedDrmException e) { + errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; + } + } + if (drmSessionManager == null) { showToast(errorStringId); return; } @@ -280,45 +303,58 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } - if (needNewPlayer || needRetrySource) { - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[] {intent.getData()}; - extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - return; + String action = intent.getAction(); + Uri[] uris; + String[] extensions; + if (ACTION_VIEW.equals(action)) { + uris = new Uri[]{intent.getData()}; + extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)}; + } else if (ACTION_VIEW_LIST.equals(action)) { + String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); + uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); } - if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { - // The player will be reinitialized if the permission is granted. - return; + extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); + if (extensions == null) { + extensions = new String[uriStrings.length]; } - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - } - MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] - : new ConcatenatingMediaSource(mediaSources); - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); - } - player.prepare(mediaSource, !haveResumePosition, false); - needRetrySource = false; - updateButtonVisibilities(); + } else { + showToast(getString(R.string.unexpected_intent_action, action)); + return; } + if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { + // The player will be reinitialized if the permission is granted. + return; + } + MediaSource[] mediaSources = new MediaSource[uris.length]; + for (int i = 0; i < uris.length; i++) { + mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + } + MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] + : new ConcatenatingMediaSource(mediaSources); + String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); + if (adTagUriString != null) { + Uri adTagUri = Uri.parse(adTagUriString); + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + try { + mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); + } catch (Exception e) { + showToast(R.string.ima_not_loaded); + } + } else { + releaseAdsLoader(); + } + boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; + if (haveResumePosition) { + player.seekTo(resumeWindow, resumePosition); + } + player.prepare(mediaSource, !haveResumePosition, false); + inErrorState = false; + updateButtonVisibilities(); } private MediaSource buildMediaSource(Uri uri, String overrideExtension) { @@ -342,11 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } - private DrmSessionManager buildDrmSessionManager(UUID uuid, + private DrmSessionManager buildDrmSessionManagerV18(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { - if (Util.SDK_INT < 18) { - return null; - } HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, buildHttpDataSourceFactory(false)); if (keyRequestPropertiesArray != null) { @@ -355,8 +388,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay keyRequestPropertiesArray[i + 1]); } } - return new DefaultDrmSessionManager<>(uuid, - FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); + return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, + null, mainHandler, eventLogger); } private void releasePlayer() { @@ -375,8 +408,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private void updateResumePosition() { resumeWindow = player.getCurrentWindowIndex(); - resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition()) - : C.TIME_UNSET; + resumePosition = Math.max(0, player.getContentPosition()); } private void clearResumePosition() { @@ -408,7 +440,48 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay .buildHttpDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null); } - // ExoPlayer.EventListener implementation + /** + * Returns an ads media source, reusing the ads loader if one exists. + * + * @throws Exception Thrown if it was not possible to create an ads media source, for example, due + * to a missing dependency. + */ + private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) throws Exception { + // Load the extension source using reflection so the demo app doesn't have to depend on it. + // The ads loader is reused for multiple playbacks, so that ad playback can resume. + Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); + if (imaAdsLoader == null) { + imaAdsLoader = loaderClass.getConstructor(Context.class, Uri.class) + .newInstance(this, adTagUri); + adOverlayViewGroup = new FrameLayout(this); + // The demo app has a non-null overlay frame layout. + simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); + } + Class sourceClass = + Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource"); + Constructor constructor = sourceClass.getConstructor(MediaSource.class, + DataSource.Factory.class, loaderClass, ViewGroup.class); + return (MediaSource) constructor.newInstance(mediaSource, mediaDataSourceFactory, imaAdsLoader, + adOverlayViewGroup); + } + + private void releaseAdsLoader() { + if (imaAdsLoader != null) { + try { + Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); + Method releaseMethod = loaderClass.getMethod("release"); + releaseMethod.invoke(imaAdsLoader); + } catch (Exception e) { + // Should never happen. + throw new IllegalStateException(e); + } + imaAdsLoader = null; + loadedAdTagUri = null; + simpleExoPlayerView.getOverlayFrameLayout().removeAllViews(); + } + } + + // Player.EventListener implementation @Override public void onLoadingChanged(boolean isLoading) { @@ -417,15 +490,20 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED) { + if (playbackState == Player.STATE_ENDED) { showControls(); } updateButtonVisibilities(); } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { - if (needRetrySource) { + if (inErrorState) { // This will only occur if the user has performed a seek whilst in the error state. Update the // resume position so that if the user then retries, playback will resume from the position to // which they seeked. @@ -471,7 +549,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (errorString != null) { showToast(errorString); } - needRetrySource = true; + inErrorState = true; if (isBehindLiveWindow(e)) { clearResumePosition(); initializePlayer(); @@ -507,7 +585,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private void updateButtonVisibilities() { debugRootView.removeAllViews(); - retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE); + retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE); debugRootView.addView(retryButton); if (player == null) { diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 081ad190b5..87b8e92e83 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity { String[] drmKeyRequestProperties = null; boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; + String adTagUri = null; reader.beginObject(); while (reader.hasNext()) { @@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity { } reader.endArray(); break; + case "ad_tag_uri": + adTagUri = reader.nextString(); + break; default: throw new ParserException("Unsupported attribute name: " + name); } @@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity { preferExtensionDecoders, playlistSamplesArray); } else { return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension); + preferExtensionDecoders, uri, extension, adTagUri); } } @@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity { public final String uri; public final String extension; + public final String adTagUri; public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, - String extension) { + String extension, String adTagUri) { super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); this.uri = uri; this.extension = extension; + this.adTagUri = adTagUri; } @Override @@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity { return super.buildIntent(context) .setData(Uri.parse(uri)) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension) + .putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri) .setAction(PlayerActivity.ACTION_VIEW); } diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index 4eb2b89324..cc6357c574 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -56,4 +56,6 @@ One or more sample lists failed to load + Playing sample without ads, as the IMA extension was not loaded + diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index a570385a52..2287c4c19b 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -1,23 +1,18 @@ -# ExoPlayer Cronet Extension # +# ExoPlayer Cronet extension # ## Description ## -[Cronet][] is Chromium's Networking stack packaged as a library. - -The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][]. +The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F -## Build Instructions ## +## Build instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` - -* Get the Cronet libraries: +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to get the Cronet libraries +and enable the extension: 1. Find the latest Cronet release [here][] and navigate to its `Release/cronet` directory @@ -27,6 +22,37 @@ git clone https://github.com/google/ExoPlayer.git 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension -* In ExoPlayer's `settings.gradle` file, uncomment the Cronet extension +* In your `settings.gradle` file, add the following line before the line that + applies `core_settings.gradle`: +```gradle +gradle.ext.exoplayerIncludeCronetExtension = true; +``` + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +If your application only needs to play http(s) content, using the Cronet +extension is as simple as updating any `DataSource`s and `DataSource.Factory` +instantiations in your application code to use `CronetDataSource` and +`CronetDataSourceFactory` respectively. If your application also needs to play +non-http(s) content such as local files, use +``` +new DefaultDataSource( + ... + new CronetDataSource(...) /* baseDataSource argument */); +``` +and +``` +new DefaultDataSourceFactory( + ... + new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */); +``` +respectively. diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 5611817b2e..930a53c7c5 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -29,11 +30,11 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile files('libs/cronet_api.jar') compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') - androidTestCompile project(':library') + androidTestCompile project(modulePrefix + 'library') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml index 2f45a1a2e5..1f371a1864 100644 --- a/extensions/cronet/src/androidTest/AndroidManifest.xml +++ b/extensions/cronet/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:targetPackage="com.google.android.exoplayer.ext.cronet"/> diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 246e23e172..06a356487e 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -124,6 +124,7 @@ public final class CronetDataSourceTest { when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) .thenReturn(mockUrlRequestBuilder); + when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder); when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest); mockStatusResponse(); @@ -683,6 +684,15 @@ public final class CronetDataSourceTest { } } + @Test + public void testAllowDirectExecutor() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + mockResponseStartSuccess(); + + dataSourceUnderTest.open(testDataSpec); + verify(mockUrlRequestBuilder).allowDirectExecutor(); + } + // Helper methods. private void mockStatusResponse() { diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 4f15a6eabc..204a2756bb 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -20,6 +20,7 @@ import android.os.ConditionVariable; import android.text.TextUtils; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -27,7 +28,6 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Predicate; -import com.google.android.exoplayer2.util.SystemClock; import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -74,6 +74,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.cronet"); + } + /** * The default connection timeout, in milliseconds. */ @@ -127,7 +131,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. + * This may be a direct executor (i.e. executes tasks on the calling thread) in order + * to avoid a thread hop from Cronet's internal network thread to the response handling + * thread. However, to avoid slowing down overall network performance, care must be taken + * to make sure response handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then an {@link InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. @@ -141,7 +149,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou /** * @param cronetEngine A CronetEngine. - * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. + * This may be a direct executor (i.e. executes tasks on the calling thread) in order + * to avoid a thread hop from Cronet's internal network thread to the response handling + * thread. However, to avoid slowing down overall network performance, care must be taken + * to make sure response handling is a fast operation when using a direct executor. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then an {@link InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. @@ -156,7 +168,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, RequestProperties defaultRequestProperties) { this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, - readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties); + readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties); } /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, @@ -416,8 +428,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou // Internal methods. private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException { - UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(), - this, executor); + UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder( + dataSpec.uri.toString(), this, executor).allowDirectExecutor(); // Set the headers. boolean isContentTypeHeaderSet = false; if (defaultRequestProperties != null) { diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index 2ad6da6a54..d6237fc988 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -16,9 +16,11 @@ package com.google.android.exoplayer2.ext.cronet; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Predicate; import java.util.concurrent.Executor; @@ -34,43 +36,143 @@ public final class CronetDataSourceFactory extends BaseFactory { */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS; + /** * The default read timeout, in milliseconds. */ public static final int DEFAULT_READ_TIMEOUT_MILLIS = CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS; - private final CronetEngine cronetEngine; + private final CronetEngineWrapper cronetEngineWrapper; private final Executor executor; private final Predicate contentTypePredicate; private final TransferListener transferListener; private final int connectTimeoutMs; private final int readTimeoutMs; private final boolean resetTimeoutOnRedirects; + private final HttpDataSource.Factory fallbackFactory; - public CronetDataSourceFactory(CronetEngine cronetEngine, + /** + * Constructs a CronetDataSourceFactory. + *

+ * 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 contentTypePredicate, - TransferListener transferListener) { - this(cronetEngine, executor, contentTypePredicate, transferListener, - DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false); + TransferListener transferListener, + HttpDataSource.Factory fallbackFactory) { + this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, + DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory); } - public CronetDataSourceFactory(CronetEngine cronetEngine, + /** + * Constructs a CronetDataSourceFactory. + *

+ * 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 contentTypePredicate, + TransferListener 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. + *

+ * 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 contentTypePredicate, TransferListener transferListener, int connectTimeoutMs, - int readTimeoutMs, boolean resetTimeoutOnRedirects) { - this.cronetEngine = cronetEngine; + int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) { + this(cronetEngineWrapper, executor, contentTypePredicate, transferListener, + DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects, + new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs, + readTimeoutMs, resetTimeoutOnRedirects)); + } + + /** + * Constructs a CronetDataSourceFactory. + *

+ * 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 contentTypePredicate, + TransferListener transferListener, int connectTimeoutMs, + int readTimeoutMs, boolean resetTimeoutOnRedirects, + HttpDataSource.Factory fallbackFactory) { + this.cronetEngineWrapper = cronetEngineWrapper; this.executor = executor; this.contentTypePredicate = contentTypePredicate; this.transferListener = transferListener; this.connectTimeoutMs = connectTimeoutMs; this.readTimeoutMs = readTimeoutMs; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; + this.fallbackFactory = fallbackFactory; } @Override - protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties + protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties defaultRequestProperties) { + CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine(); + if (cronetEngine == null) { + return fallbackFactory.createDataSource(); + } return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); } diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java new file mode 100644 index 0000000000..efe30d6525 --- /dev/null +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java @@ -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 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 { + + 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; + } + } + +} diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 4ce9173ec9..b4514effbc 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -1,4 +1,4 @@ -# FfmpegAudioRenderer # +# ExoPlayer FFmpeg extension # ## Description ## @@ -9,11 +9,10 @@ audio. ## Build instructions ## -* Checkout ExoPlayer along with Extensions - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` -* In your project, you can add a dependency on the extension by using a rule - like this: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-ffmpeg' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-ffmpeg') -} -``` - -* Now, when you build your app, the extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 0eddd017a4..9820818f3e 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -30,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') } ext { diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 8d75ca3dbb..453a18476e 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -30,7 +30,14 @@ import com.google.android.exoplayer2.util.MimeTypes; */ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { + /** + * The number of input and output buffers. + */ private static final int NUM_BUFFERS = 16; + /** + * The initial input buffer size. Input buffers are reallocated dynamically if this value is + * insufficient. + */ private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; private FfmpegDecoder decoder; diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 4992bcbb3e..9b3bbbb6ab 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.MimeTypes; @@ -23,6 +24,10 @@ import com.google.android.exoplayer2.util.MimeTypes; */ public final class FfmpegLibrary { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg"); + } + private static final LibraryLoader LOADER = new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg"); @@ -32,6 +37,8 @@ public final class FfmpegLibrary { * Override the names of the FFmpeg native libraries. If an application wishes to call this * method, it must do so before calling any other method defined by this class, and before * instantiating a {@link FfmpegAudioRenderer} instance. + * + * @param libraries The names of the FFmpeg native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); @@ -53,6 +60,8 @@ public final class FfmpegLibrary { /** * Returns whether the underlying library supports the specified MIME type. + * + * @param mimeType The MIME type to check. */ public static boolean supportsFormat(String mimeType) { if (!isAvailable()) { diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 2f3b067d6f..9db2e5727d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -1,20 +1,19 @@ -# ExoPlayer Flac Extension # +# ExoPlayer Flac extension # ## Description ## -The Flac Extension is a [Renderer][] implementation that helps you bundle +The Flac extension is a [Renderer][] implementation that helps you bundle libFLAC (the Flac decoding library) into your app and use it along with ExoPlayer to play Flac audio on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=all -j4 ``` -* In your project, you can add a dependency to the Flac Extension by using a - rule like this: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-flac' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-flac') -} -``` - -* Now, when you build your app, the Flac extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 4a6b8e0e5a..4d840d34ac 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -30,8 +31,8 @@ android { } dependencies { - compile project(':library-core') - androidTestCompile project(':testutils') + compile project(modulePrefix + 'library-core') + androidTestCompile project(modulePrefix + 'testutils') } ext { diff --git a/extensions/flac/src/androidTest/AndroidManifest.xml b/extensions/flac/src/androidTest/AndroidManifest.xml index 0a62db3bb5..73032ab50c 100644 --- a/extensions/flac/src/androidTest/AndroidManifest.xml +++ b/extensions/flac/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index 4196f1ea63..7b193997c3 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link FlacExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public class FlacExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new FlacExtractor(); diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 21f01f0cca..1fa30bed9d 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -57,7 +58,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener { + private static class TestPlaybackThread extends Thread implements Player.EventListener { private final Context context; private final Uri uri; @@ -120,12 +121,17 @@ public class FlacPlaybackTest extends InstrumentationTestCase { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playbackException != null)) { releasePlayerAndQuitLooper(); } } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index d13194793e..7b71b5c743 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -159,13 +159,17 @@ public final class FlacExtractor implements Extractor { if (position == 0) { metadataParsed = false; } - decoderJni.reset(position); + if (decoderJni != null) { + decoderJni.reset(position); + } } @Override public void release() { - decoderJni.release(); - decoderJni = null; + if (decoderJni != null) { + decoderJni.release(); + decoderJni = null; + } } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java index ca18051207..d8b9b808a6 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.flac; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; /** @@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader; */ public final class FlacLibrary { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.flac"); + } + private static final LibraryLoader LOADER = new LibraryLoader("flacJNI"); private FlacLibrary() {} @@ -30,6 +35,8 @@ public final class FlacLibrary { * Override the names of the Flac native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating * any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances. + * + * @param libraries The names of the Flac native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index bae5de4812..7e072d070c 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -1,4 +1,4 @@ -# ExoPlayer GVR Extension # +# ExoPlayer GVR extension # ## Description ## @@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering of surround sound and ambisonic soundfields. -## Using the extension ## +[Google VR SDK for Android]: https://developers.google.com/vr/android/ +[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround + +## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency. You need to make sure you have the jcenter repository included in the `build.gradle` @@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' where `rX.X.X` is the version, which must match the version of the ExoPlayer library being used. -## Using GvrAudioProcessor ## +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +## Using the extension ## * If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to return a GvrAudioProcessor. * If constructing renderers directly, pass a GvrAudioProcessor to MediaCodecAudioRenderer's constructor. -[Google VR SDK for Android]: https://developers.google.com/vr/android/ -[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index e15c8b1ad8..66665576bb 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -24,7 +25,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.google.vr:sdk-audio:1.60.1' } diff --git a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java index a56bc7f0a9..5750f5f04d 100644 --- a/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java +++ b/extensions/gvr/src/main/java/com/google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.gvr; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.vr.sdk.audio.GvrAudioSurround; @@ -28,6 +29,10 @@ import java.nio.ByteOrder; */ public final class GvrAudioProcessor implements AudioProcessor { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.gvr"); + } + private static final int FRAMES_PER_OUTPUT_BUFFER = 1024; private static final int OUTPUT_CHANNEL_COUNT = 2; private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. @@ -56,6 +61,11 @@ public final class GvrAudioProcessor implements AudioProcessor { /** * Updates the listener head orientation. May be called on any thread. See * {@code GvrAudioSurround.updateNativeOrientation}. + * + * @param w The w component of the quaternion. + * @param x The x component of the quaternion. + * @param y The y component of the quaternion. + * @param z The z component of the quaternion. */ public synchronized void updateOrientation(float w, float x, float y, float z) { this.w = w; diff --git a/extensions/ima/README.md b/extensions/ima/README.md new file mode 100644 index 0000000000..f328bb44cb --- /dev/null +++ b/extensions/ima/README.md @@ -0,0 +1,57 @@ +# ExoPlayer IMA extension # + +## Description ## + +The IMA extension is a [MediaSource][] implementation wrapping the +[Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads +alongside content. + +[IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ +[MediaSource]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/MediaSource.html + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +compile 'com.google.android.exoplayer:extension-ima:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +To play ads alongside a single-window content `MediaSource`, prepare the player +with an `ImaAdsMediaSource` constructed using an `ImaAdsLoader`, the content +`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag +URI from your ad campaign when creating the `ImaAdsLoader`. The IMA +documentation includes some [sample ad tags][] for testing. + +Resuming the player after entering the background requires some special handling +when playing ads. The player and its media source are released on entering the +background, and are recreated when the player returns to the foreground. When +playing ads it is necessary to persist ad playback state while in the background +by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of +the same content/ads by passing it in when constructing the new +`ImaAdsMediaSource`. It is also important to persist the player position when +entering the background by storing the value of `player.getContentPosition()`. +On returning to the foreground, seek to that position before preparing the new +player instance. Finally, it is important to call `ImaAdsLoader.release()` when +playback of the content/ads has finished and will not be resumed. + +You can try the IMA extension in the ExoPlayer demo app. To do this you must +select and build one of the `withExtensions` build variants of the demo app in +Android Studio. You can find IMA test content in the "IMA sample ad tags" +section of the app. The demo app's `PlayerActivity` also shows how to persist +the `ImaAdsLoader` instance and the player position when backgrounded during ad +playback. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle new file mode 100644 index 0000000000..a4ead9e01f --- /dev/null +++ b/extensions/ima/build.gradle @@ -0,0 +1,42 @@ +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 14 + targetSdkVersion project.ext.targetSdkVersion + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + compile project(modulePrefix + 'library-core') + // This dependency is necessary to force the supportLibraryVersion of + // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via: + // com.google.android.gms:play-services-ads:11.0.2 + // |-- com.google.android.gms:play-services-ads-lite:[11.0.2] -> 11.0.2 + // |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2 + // |-- com.android.support:support-v4:25.2.0 + compile 'com.android.support:support-v4:' + supportLibraryVersion + compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' + compile 'com.google.android.gms:play-services-ads:11.0.2' + androidTestCompile project(modulePrefix + 'library') + androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion +} + +ext { + javadocTitle = 'IMA extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-ima' + releaseDescription = 'Interactive Media Ads extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/ima/src/main/AndroidManifest.xml b/extensions/ima/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..22fb518c58 --- /dev/null +++ b/extensions/ima/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java new file mode 100644 index 0000000000..0edd7d6558 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import java.util.Arrays; + +/** + * Represents the structure of ads to play and the state of loaded/played ads. + */ +/* package */ final class AdPlaybackState { + + /** + * The number of ad groups. + */ + public final int adGroupCount; + /** + * The times of ad groups, in microseconds. A final element with the value + * {@link C#TIME_END_OF_SOURCE} indicates a postroll ad. + */ + public final long[] adGroupTimesUs; + /** + * The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} if the number of + * ads is not yet known. + */ + public final int[] adCounts; + /** + * The number of ads loaded so far in each ad group. + */ + public final int[] adsLoadedCounts; + /** + * The number of ads played so far in each ad group. + */ + public final int[] adsPlayedCounts; + /** + * The URI of each ad in each ad group. + */ + public final Uri[][] adUris; + + /** + * The position offset in the first unplayed ad at which to begin playback, in microseconds. + */ + public long adResumePositionUs; + + /** + * Creates a new ad playback state with the specified ad group times. + * + * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value + * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. + */ + public AdPlaybackState(long[] adGroupTimesUs) { + this.adGroupTimesUs = adGroupTimesUs; + adGroupCount = adGroupTimesUs.length; + adsPlayedCounts = new int[adGroupCount]; + adCounts = new int[adGroupCount]; + Arrays.fill(adCounts, C.LENGTH_UNSET); + adUris = new Uri[adGroupCount][]; + Arrays.fill(adUris, new Uri[0]); + adsLoadedCounts = new int[adGroupTimesUs.length]; + } + + private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, + int[] adsPlayedCounts, Uri[][] adUris, long adResumePositionUs) { + this.adGroupTimesUs = adGroupTimesUs; + this.adCounts = adCounts; + this.adsLoadedCounts = adsLoadedCounts; + this.adsPlayedCounts = adsPlayedCounts; + this.adUris = adUris; + this.adResumePositionUs = adResumePositionUs; + adGroupCount = adGroupTimesUs.length; + } + + /** + * Returns a deep copy of this instance. + */ + public AdPlaybackState copy() { + Uri[][] adUris = new Uri[adGroupTimesUs.length][]; + for (int i = 0; i < this.adUris.length; i++) { + adUris[i] = Arrays.copyOf(this.adUris[i], this.adUris[i].length); + } + return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount), + Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount), + Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, adResumePositionUs); + } + + /** + * Sets the number of ads in the specified ad group. + */ + public void setAdCount(int adGroupIndex, int adCount) { + adCounts[adGroupIndex] = adCount; + } + + /** + * Adds an ad to the specified ad group. + */ + public void addAdUri(int adGroupIndex, Uri uri) { + int adIndexInAdGroup = adUris[adGroupIndex].length; + adUris[adGroupIndex] = Arrays.copyOf(adUris[adGroupIndex], adIndexInAdGroup + 1); + adUris[adGroupIndex][adIndexInAdGroup] = uri; + adsLoadedCounts[adGroupIndex]++; + } + + /** + * Marks the last ad in the specified ad group as played. + */ + public void playedAd(int adGroupIndex) { + adResumePositionUs = 0; + adsPlayedCounts[adGroupIndex]++; + } + + /** + * Marks all ads in the specified ad group as played. + */ + public void playedAdGroup(int adGroupIndex) { + adResumePositionUs = 0; + if (adCounts[adGroupIndex] == C.LENGTH_UNSET) { + adCounts[adGroupIndex] = 0; + } + adsPlayedCounts[adGroupIndex] = adCounts[adGroupIndex]; + } + + /** + * Sets the position offset in the first unplayed ad at which to begin playback, in microseconds. + */ + public void setAdResumePositionUs(long adResumePositionUs) { + this.adResumePositionUs = adResumePositionUs; + } + +} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java new file mode 100644 index 0000000000..8c4fb4c51c --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -0,0 +1,728 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import android.content.Context; +import android.net.Uri; +import android.os.SystemClock; +import android.util.Log; +import android.view.ViewGroup; +import com.google.ads.interactivemedia.v3.api.Ad; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdErrorEvent; +import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; +import com.google.ads.interactivemedia.v3.api.AdEvent; +import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; +import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; +import com.google.ads.interactivemedia.v3.api.AdPodInfo; +import com.google.ads.interactivemedia.v3.api.AdsLoader; +import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; +import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; +import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Loads ads using the IMA SDK. All methods are called on the main thread. + */ +public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, + ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { + + /** + * Listener for ad loader events. All methods are called on the main thread. + */ + /* package */ interface EventListener { + + /** + * Called when the ad playback state has been updated. + * + * @param adPlaybackState The new ad playback state. + */ + void onAdPlaybackState(AdPlaybackState adPlaybackState); + + /** + * Called when there was an error loading ads. + * + * @param error The error. + */ + void onLoadError(IOException error); + + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + + } + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); + } + + private static final boolean DEBUG = false; + private static final String TAG = "ImaAdsLoader"; + + /** + * Whether to enable preloading of ads in {@link AdsRenderingSettings}. + */ + private static final boolean ENABLE_PRELOADING = true; + + private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; + private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + + /** + * Threshold before the end of content at which IMA is notified that content is complete if the + * player buffers, in milliseconds. + */ + private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; + + private final Uri adTagUri; + private final Timeline.Period period; + private final List adCallbacks; + private final ImaSdkFactory imaSdkFactory; + private final AdDisplayContainer adDisplayContainer; + private final AdsLoader adsLoader; + + private EventListener eventListener; + private Player player; + private VideoProgressUpdate lastContentProgress; + private VideoProgressUpdate lastAdProgress; + + private AdsManager adsManager; + private Timeline timeline; + private long contentDurationMs; + private AdPlaybackState adPlaybackState; + + // Fields tracking IMA's state. + + /** + * The index of the current ad group that IMA is loading. + */ + private int adGroupIndex; + /** + * Whether IMA has sent an ad event to pause content since the last resume content event. + */ + private boolean imaPausedContent; + /** + * If {@link #playingAd} is set, stores whether IMA has called {@link #playAd()} and not + * {@link #stopAd()}. + */ + private boolean imaPlayingAd; + /** + * If {@link #playingAd} is set, stores whether IMA has called {@link #pauseAd()} since a + * preceding call to {@link #playAd()} for the current ad. + */ + private boolean imaPausedInAd; + /** + * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. + */ + private boolean sentContentComplete; + + // Fields tracking the player/loader state. + + /** + * Whether the player's play when ready flag has temporarily been set to true for playing ads. + */ + private boolean playWhenReadyOverriddenForAds; + /** + * Whether the player is playing an ad. + */ + private boolean playingAd; + /** + * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} + * otherwise. + */ + private int playingAdIndexInAdGroup; + /** + * If a content period has finished but IMA has not yet sent an ad event with + * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of + * {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to + * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. + */ + private long fakeContentProgressElapsedRealtimeMs; + /** + * If {@link #fakeContentProgressElapsedRealtimeMs} is set, stores the offset from which the + * content progress should increase. {@link C#TIME_UNSET} otherwise. + */ + private long fakeContentProgressOffsetMs; + /** + * Stores the pending content position when a seek operation was intercepted to play an ad. + */ + private long pendingContentPositionMs; + /** + * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. + */ + private boolean sentPendingContentPositionMs; + /** + * Whether {@link #release()} has been called. + */ + private boolean released; + + /** + * Creates a new IMA ads loader. + * + * @param context The context. + * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * more information. + */ + public ImaAdsLoader(Context context, Uri adTagUri) { + this(context, adTagUri, null); + } + + /** + * Creates a new IMA ads loader. + * + * @param context The context. + * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * more information. + * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to + * use the default settings. If set, the player type and version fields may be overwritten. + */ + public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { + this.adTagUri = adTagUri; + period = new Timeline.Period(); + adCallbacks = new ArrayList<>(1); + imaSdkFactory = ImaSdkFactory.getInstance(); + adDisplayContainer = imaSdkFactory.createAdDisplayContainer(); + adDisplayContainer.setPlayer(this); + if (imaSdkSettings == null) { + imaSdkSettings = imaSdkFactory.createImaSdkSettings(); + } + imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); + imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); + adsLoader = imaSdkFactory.createAdsLoader(context, imaSdkSettings); + adsLoader.addAdErrorListener(this); + adsLoader.addAdsLoadedListener(this); + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + fakeContentProgressOffsetMs = C.TIME_UNSET; + pendingContentPositionMs = C.TIME_UNSET; + adGroupIndex = C.INDEX_UNSET; + contentDurationMs = C.TIME_UNSET; + } + + /** + * Attaches a player that will play ads loaded using this instance. + * + * @param player The player instance that will play the loaded ads. + * @param eventListener Listener for ads loader events. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + /* package */ void attachPlayer(ExoPlayer player, EventListener eventListener, + ViewGroup adUiViewGroup) { + this.player = player; + this.eventListener = eventListener; + lastAdProgress = null; + lastContentProgress = null; + adDisplayContainer.setAdContainer(adUiViewGroup); + player.addListener(this); + if (adPlaybackState != null) { + eventListener.onAdPlaybackState(adPlaybackState); + if (imaPausedContent) { + adsManager.resume(); + } + } else { + requestAds(); + } + } + + /** + * Detaches the attached player and event listener. To attach a new player, call + * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Call {@link #release()} to release + * all resources associated with this instance. + */ + /* package */ void detachPlayer() { + if (adsManager != null && imaPausedContent) { + adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); + adsManager.pause(); + } + lastAdProgress = getAdProgress(); + lastContentProgress = getContentProgress(); + player.removeListener(this); + player = null; + eventListener = null; + } + + /** + * Releases the loader. Must be called when the instance is no longer needed. + */ + public void release() { + released = true; + if (adsManager != null) { + adsManager.destroy(); + adsManager = null; + } + } + + // AdsLoader.AdsLoadedListener implementation. + + @Override + public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { + AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); + if (released) { + adsManager.destroy(); + return; + } + this.adsManager = adsManager; + adsManager.addAdErrorListener(this); + adsManager.addAdEventListener(this); + if (ENABLE_PRELOADING) { + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(true); + adsManager.init(adsRenderingSettings); + if (DEBUG) { + Log.d(TAG, "Initialized with preloading"); + } + } else { + adsManager.init(); + if (DEBUG) { + Log.d(TAG, "Initialized without preloading"); + } + } + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + updateAdPlaybackState(); + } + + // AdEvent.AdEventListener implementation. + + @Override + public void onAdEvent(AdEvent adEvent) { + AdEventType adEventType = adEvent.getType(); + boolean isLogAdEvent = adEventType == AdEventType.LOG; + if (DEBUG || isLogAdEvent) { + Log.w(TAG, "onAdEvent: " + adEventType); + if (isLogAdEvent) { + for (Map.Entry entry : adEvent.getAdData().entrySet()) { + Log.w(TAG, " " + entry.getKey() + ": " + entry.getValue()); + } + } + } + if (adsManager == null) { + Log.w(TAG, "Dropping ad event after release: " + adEvent); + return; + } + Ad ad = adEvent.getAd(); + switch (adEvent.getType()) { + case LOADED: + // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. + AdPodInfo adPodInfo = ad.getAdPodInfo(); + int podIndex = adPodInfo.getPodIndex(); + adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex; + int adPosition = adPodInfo.getAdPosition(); + int adCountInAdGroup = adPodInfo.getTotalAds(); + adsManager.start(); + if (DEBUG) { + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " + + adGroupIndex); + } + adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); + updateAdPlaybackState(); + break; + case CONTENT_PAUSE_REQUESTED: + // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads + // before sending CONTENT_RESUME_REQUESTED. + imaPausedContent = true; + pauseContentInternal(); + break; + case TAPPED: + if (eventListener != null) { + eventListener.onAdTapped(); + } + break; + case CLICKED: + if (eventListener != null) { + eventListener.onAdClicked(); + } + break; + case CONTENT_RESUME_REQUESTED: + imaPausedContent = false; + resumeContentInternal(); + break; + case ALL_ADS_COMPLETED: + // Do nothing. The ads manager will be released when the source is released. + default: + break; + } + } + + // AdErrorEvent.AdErrorListener implementation. + + @Override + public void onAdError(AdErrorEvent adErrorEvent) { + if (DEBUG) { + Log.d(TAG, "onAdError " + adErrorEvent); + } + if (adsManager == null) { + adPlaybackState = new AdPlaybackState(new long[0]); + updateAdPlaybackState(); + } + if (eventListener != null) { + IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); + eventListener.onLoadError(exception); + } + } + + // ContentProgressProvider implementation. + + @Override + public VideoProgressUpdate getContentProgress() { + if (player == null) { + return lastContentProgress; + } else if (pendingContentPositionMs != C.TIME_UNSET) { + sentPendingContentPositionMs = true; + return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs); + } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { + long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; + long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; + return new VideoProgressUpdate(fakePositionMs, contentDurationMs); + } else if (playingAd || contentDurationMs == C.TIME_UNSET) { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } else { + return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); + } + } + + // VideoAdPlayer implementation. + + @Override + public VideoProgressUpdate getAdProgress() { + if (player == null) { + return lastAdProgress; + } else if (!playingAd) { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + } else { + return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); + } + } + + @Override + public void loadAd(String adUriString) { + if (DEBUG) { + Log.d(TAG, "loadAd in ad group " + adGroupIndex); + } + adPlaybackState.addAdUri(adGroupIndex, Uri.parse(adUriString)); + updateAdPlaybackState(); + } + + @Override + public void addCallback(VideoAdPlayerCallback videoAdPlayerCallback) { + adCallbacks.add(videoAdPlayerCallback); + } + + @Override + public void removeCallback(VideoAdPlayerCallback videoAdPlayerCallback) { + adCallbacks.remove(videoAdPlayerCallback); + } + + @Override + public void playAd() { + if (DEBUG) { + Log.d(TAG, "playAd"); + } + if (player == null) { + // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. + Log.w(TAG, "Unexpected playAd while detached"); + } else if (!player.getPlayWhenReady()) { + playWhenReadyOverriddenForAds = true; + player.setPlayWhenReady(true); + } + if (imaPlayingAd && !imaPausedInAd) { + // Work around an issue where IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028, b/63320878]. + Log.w(TAG, "Unexpected playAd without stopAd"); + } + if (!imaPlayingAd) { + imaPlayingAd = true; + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onPlay(); + } + } else if (imaPausedInAd) { + imaPausedInAd = false; + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onResume(); + } + } + } + + @Override + public void stopAd() { + if (DEBUG) { + Log.d(TAG, "stopAd"); + } + if (player == null) { + // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. + Log.w(TAG, "Unexpected stopAd while detached"); + } + if (!imaPlayingAd) { + Log.w(TAG, "Unexpected stopAd"); + return; + } + stopAdInternal(); + } + + @Override + public void pauseAd() { + if (DEBUG) { + Log.d(TAG, "pauseAd"); + } + if (!imaPlayingAd) { + // This method is called after content is resumed. + return; + } + imaPausedInAd = true; + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onPause(); + } + } + + @Override + public void resumeAd() { + // This method is never called. See [Internal: b/18931719]. + throw new IllegalStateException(); + } + + // Player.EventListener implementation. + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + if (timeline.isEmpty()) { + // The player is being re-prepared and this source will be released. + return; + } + Assertions.checkArgument(timeline.getPeriodCount() == 1); + this.timeline = timeline; + contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs); + updateImaStateForPlayerState(); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (adsManager == null) { + return; + } + + if (!imaPlayingAd && playbackState == Player.STATE_BUFFERING && playWhenReady) { + checkForContentComplete(); + } else if (imaPlayingAd && playbackState == Player.STATE_ENDED) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onEnded(); + } + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + if (playingAd) { + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onError(); + } + } + } + + @Override + public void onPositionDiscontinuity() { + if (adsManager == null) { + return; + } + if (!playingAd && !player.isPlayingAd()) { + checkForContentComplete(); + if (sentContentComplete) { + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { + adPlaybackState.playedAdGroup(i); + } + } + updateAdPlaybackState(); + } else { + long positionMs = player.getCurrentPosition(); + timeline.getPeriod(0, period); + if (period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)) != C.INDEX_UNSET) { + sentPendingContentPositionMs = false; + pendingContentPositionMs = positionMs; + } + } + } else { + updateImaStateForPlayerState(); + } + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + + // Internal methods. + + private void requestAds() { + AdsRequest request = imaSdkFactory.createAdsRequest(); + request.setAdTagUrl(adTagUri.toString()); + request.setAdDisplayContainer(adDisplayContainer); + request.setContentProgressProvider(this); + adsLoader.requestAds(request); + } + + private void updateImaStateForPlayerState() { + boolean wasPlayingAd = playingAd; + playingAd = player.isPlayingAd(); + if (!playingAd && playWhenReadyOverriddenForAds) { + playWhenReadyOverriddenForAds = false; + player.setPlayWhenReady(false); + } + if (!sentContentComplete) { + boolean adFinished = (wasPlayingAd && !playingAd) + || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); + if (adFinished) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onEnded(); + } + } + if (!wasPlayingAd && playingAd) { + int adGroupIndex = player.getCurrentAdGroupIndex(); + // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. + Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); + if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { + fakeContentProgressOffsetMs = contentDurationMs; + } + } + } + playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; + } + + private void resumeContentInternal() { + if (imaPlayingAd) { + if (DEBUG) { + Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); + } + } + if (playingAd && adGroupIndex != C.INDEX_UNSET) { + adPlaybackState.playedAdGroup(adGroupIndex); + adGroupIndex = C.INDEX_UNSET; + updateAdPlaybackState(); + } + clearFlags(); + } + + private void pauseContentInternal() { + if (sentPendingContentPositionMs) { + pendingContentPositionMs = C.TIME_UNSET; + sentPendingContentPositionMs = false; + } + // IMA is requesting to pause content, so stop faking the content position. + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + fakeContentProgressOffsetMs = C.TIME_UNSET; + clearFlags(); + } + + private void stopAdInternal() { + Assertions.checkState(imaPlayingAd); + adPlaybackState.playedAd(adGroupIndex); + updateAdPlaybackState(); + if (!playingAd) { + adGroupIndex = C.INDEX_UNSET; + } + clearFlags(); + } + + private void clearFlags() { + // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until + // the content is resumed. + imaPlayingAd = false; + imaPausedInAd = false; + } + + private void checkForContentComplete() { + if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET + && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs + && !sentContentComplete) { + adsLoader.contentComplete(); + if (DEBUG) { + Log.d(TAG, "adsLoader.contentComplete"); + } + sentContentComplete = true; + } + } + + private void updateAdPlaybackState() { + // Ignore updates while detached. When a player is attached it will receive the latest state. + if (eventListener != null) { + eventListener.onAdPlaybackState(adPlaybackState.copy()); + } + } + + private static long[] getAdGroupTimesUs(List 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; + } + +} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java new file mode 100644 index 0000000000..d56a3ad41f --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.ViewGroup; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link MediaSource} that inserts ads linearly with a provided content media source using the + * Interactive Media Ads SDK for ad loading and tracking. + */ +public final class ImaAdsMediaSource implements MediaSource { + + /** + * Listener for events relating to ad loading. + */ + public interface AdsListener { + + /** + * Called if there was an error loading ads. The media source will load the content without ads + * if ads can't be loaded, so listen for this event if you need to implement additional handling + * (for example, stopping the player). + * + * @param error The error. + */ + void onAdLoadError(IOException error); + + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + + } + + private static final String TAG = "ImaAdsMediaSource"; + + private final MediaSource contentMediaSource; + private final DataSource.Factory dataSourceFactory; + private final ImaAdsLoader imaAdsLoader; + private final ViewGroup adUiViewGroup; + private final Handler mainHandler; + private final AdsLoaderListener adsLoaderListener; + private final Map adMediaSourceByMediaPeriod; + private final Timeline.Period period; + @Nullable + private final Handler eventHandler; + @Nullable + private final AdsListener eventListener; + + private Handler playerHandler; + private ExoPlayer player; + private volatile boolean released; + + // Accessed on the player thread. + private Timeline contentTimeline; + private Object contentManifest; + private AdPlaybackState adPlaybackState; + private MediaSource[][] adGroupMediaSources; + private long[][] adDurationsUs; + private MediaSource.Listener listener; + + /** + * Constructs a new source that inserts ads linearly with the content specified by + * {@code contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param dataSourceFactory Factory for data sources used to load ad media. + * @param imaAdsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup) { + this(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, null, null); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by + * {@code contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param dataSourceFactory Factory for data sources used to load ad media. + * @param imaAdsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, + @Nullable AdsListener eventListener) { + this.contentMediaSource = contentMediaSource; + this.dataSourceFactory = dataSourceFactory; + this.imaAdsLoader = imaAdsLoader; + this.adUiViewGroup = adUiViewGroup; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + mainHandler = new Handler(Looper.getMainLooper()); + adsLoaderListener = new AdsLoaderListener(); + adMediaSourceByMediaPeriod = new HashMap<>(); + period = new Timeline.Period(); + adGroupMediaSources = new MediaSource[0][]; + adDurationsUs = new long[0][]; + } + + @Override + public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkArgument(isTopLevelSource); + this.listener = listener; + this.player = player; + playerHandler = new Handler(); + contentMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest); + } + }); + mainHandler.post(new Runnable() { + @Override + public void run() { + imaAdsLoader.attachPlayer(player, adsLoaderListener, adUiViewGroup); + } + }); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + contentMediaSource.maybeThrowSourceInfoRefreshError(); + for (MediaSource[] mediaSources : adGroupMediaSources) { + for (MediaSource mediaSource : mediaSources) { + if (mediaSource != null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + } + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + if (adPlaybackState.adGroupCount > 0 && id.isAd()) { + final int adGroupIndex = id.adGroupIndex; + final int adIndexInAdGroup = id.adIndexInAdGroup; + if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { + MediaSource adMediaSource = new ExtractorMediaSource( + adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, + new DefaultExtractorsFactory(), mainHandler, adsLoaderListener); + int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; + if (adIndexInAdGroup >= oldAdCount) { + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = + Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount); + Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); + } + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; + adMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + } + }); + } + MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); + adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); + return mediaPeriod; + } else { + return contentMediaSource.createPeriod(id, allocator); + } + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { + adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); + } else { + contentMediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void releaseSource() { + released = true; + contentMediaSource.releaseSource(); + for (MediaSource[] mediaSources : adGroupMediaSources) { + for (MediaSource mediaSource : mediaSources) { + if (mediaSource != null) { + mediaSource.releaseSource(); + } + } + } + mainHandler.post(new Runnable() { + @Override + public void run() { + imaAdsLoader.detachPlayer(); + } + }); + } + + // Internal methods. + + private void onAdPlaybackState(AdPlaybackState adPlaybackState) { + if (this.adPlaybackState == null) { + adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; + Arrays.fill(adGroupMediaSources, new MediaSource[0]); + adDurationsUs = new long[adPlaybackState.adGroupCount][]; + Arrays.fill(adDurationsUs, new long[0]); + } + this.adPlaybackState = adPlaybackState; + maybeUpdateSourceInfo(); + } + + private void onLoadError(final IOException error) { + Log.w(TAG, "Ad load error", error); + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdLoadError(error); + } + } + }); + } + } + + private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { + contentTimeline = timeline; + contentManifest = manifest; + maybeUpdateSourceInfo(); + } + + private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + maybeUpdateSourceInfo(); + } + + private void maybeUpdateSourceInfo() { + if (adPlaybackState != null && contentTimeline != null) { + Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline + : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, + adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, + adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); + listener.onSourceInfoRefreshed(timeline, contentManifest); + } + } + + /** + * Listener for ad loading events. All methods are called on the main thread. + */ + private final class AdsLoaderListener implements ImaAdsLoader.EventListener, + ExtractorMediaSource.EventListener { + + @Override + public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + ImaAdsMediaSource.this.onAdPlaybackState(adPlaybackState); + } + }); + } + + @Override + public void onLoadError(final IOException error) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + ImaAdsMediaSource.this.onLoadError(error); + } + }); + } + + @Override + public void onAdClicked() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdClicked(); + } + } + }); + } + } + + @Override + public void onAdTapped() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdTapped(); + } + } + }); + } + } + + } + +} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java new file mode 100644 index 0000000000..1d73234286 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A {@link Timeline} for sources that have ads. + */ +/* package */ final class SinglePeriodAdTimeline extends Timeline { + + private final Timeline contentTimeline; + private final long[] adGroupTimesUs; + private final int[] adCounts; + private final int[] adsLoadedCounts; + private final int[] adsPlayedCounts; + private final long[][] adDurationsUs; + private final long adResumePositionUs; + + /** + * Creates a new timeline with a single period containing the specified ads. + * + * @param contentTimeline The timeline of the content alongside which ads will be played. It must + * have one window and one period. + * @param adGroupTimesUs The times of ad groups relative to the start of the period, in + * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that + * the period has a postroll ad. + * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} + * if the number of ads is not yet known. + * @param adsLoadedCounts The number of ads loaded so far in each ad group. + * @param adsPlayedCounts The number of ads played so far in each ad group. + * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element + * may be {@link C#TIME_UNSET} if the duration is not yet known. + * @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin + * playback, in microseconds. + */ + public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts, + int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, + long adResumePositionUs) { + Assertions.checkState(contentTimeline.getPeriodCount() == 1); + Assertions.checkState(contentTimeline.getWindowCount() == 1); + this.contentTimeline = contentTimeline; + this.adGroupTimesUs = adGroupTimesUs; + this.adCounts = adCounts; + this.adsLoadedCounts = adsLoadedCounts; + this.adsPlayedCounts = adsPlayedCounts; + this.adDurationsUs = adDurationsUs; + this.adResumePositionUs = adResumePositionUs; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + contentTimeline.getPeriod(periodIndex, period, setIds); + period.set(period.id, period.uid, period.windowIndex, period.durationUs, + period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts, + adDurationsUs, adResumePositionUs); + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return contentTimeline.getIndexOfPeriod(uid); + } + +} diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md new file mode 100644 index 0000000000..3acf8e4c79 --- /dev/null +++ b/extensions/mediasession/README.md @@ -0,0 +1,27 @@ +# ExoPlayer MediaSession extension # + +## Description ## + +The MediaSession extension mediates between an ExoPlayer instance and a +[MediaSession][]. It automatically retrieves and implements playback actions +and syncs the player state with the state of the media session. The behaviour +can be extended to support other playback and custom actions. + +[MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +compile 'com.google.android.exoplayer:extension-mediasession:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle new file mode 100644 index 0000000000..85a8ac46e2 --- /dev/null +++ b/extensions/mediasession/build.gradle @@ -0,0 +1,42 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + compile project(modulePrefix + 'library-core') + compile 'com.android.support:support-media-compat:' + supportLibraryVersion + compile 'com.android.support:appcompat-v7:' + supportLibraryVersion +} + +ext { + javadocTitle = 'Media session extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-mediasession' + releaseDescription = 'Media session extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/mediasession/src/main/AndroidManifest.xml b/extensions/mediasession/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8ed6ef2011 --- /dev/null +++ b/extensions/mediasession/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java new file mode 100644 index 0000000000..c3586b29e6 --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.mediasession; + +import android.support.v4.media.session.PlaybackStateCompat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; + +/** + * A default implementation of {@link MediaSessionConnector.PlaybackController}. + *

+ * Methods can be safely overridden by subclasses to intercept calls for given actions. + */ +public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController { + + /** + * The default fast forward increment, in milliseconds. + */ + public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** + * The default rewind increment, in milliseconds. + */ + public static final int DEFAULT_REWIND_MS = 5000; + + private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_STOP; + + protected final long rewindIncrementMs; + protected final long fastForwardIncrementMs; + + /** + * Creates a new instance. + *

+ * Equivalent to {@code DefaultPlaybackController( + * DefaultPlaybackController.DEFAULT_REWIND_MS, + * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}. + */ + public DefaultPlaybackController() { + this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS); + } + + /** + * Creates a new instance with the given fast forward and rewind increments. + * + * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will + * cause the rewind action to be disabled. + * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative + * value will cause the fast forward action to be removed. + */ + public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) { + this.rewindIncrementMs = rewindIncrementMs; + this.fastForwardIncrementMs = fastForwardIncrementMs; + } + + @Override + public long getSupportedPlaybackActions(Player player) { + if (player == null || player.getCurrentTimeline().isEmpty()) { + return 0; + } + long actions = BASE_ACTIONS; + if (player.isCurrentWindowSeekable()) { + actions |= PlaybackStateCompat.ACTION_SEEK_TO; + } + if (fastForwardIncrementMs > 0) { + actions |= PlaybackStateCompat.ACTION_FAST_FORWARD; + } + if (rewindIncrementMs > 0) { + actions |= PlaybackStateCompat.ACTION_REWIND; + } + return actions; + } + + @Override + public void onPlay(Player player) { + player.setPlayWhenReady(true); + } + + @Override + public void onPause(Player player) { + player.setPlayWhenReady(false); + } + + @Override + public void onSeekTo(Player player, long position) { + long duration = player.getDuration(); + if (duration != C.TIME_UNSET) { + position = Math.min(position, duration); + } + player.seekTo(Math.max(position, 0)); + } + + @Override + public void onFastForward(Player player) { + if (fastForwardIncrementMs <= 0) { + return; + } + onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs); + } + + @Override + public void onRewind(Player player) { + if (rewindIncrementMs <= 0) { + return; + } + onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs); + } + + @Override + public void onStop(Player player) { + player.stop(); + } + +} diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java new file mode 100644 index 0000000000..0e839b8083 --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -0,0 +1,836 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.mediasession; + +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.ResultReceiver; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Connects a {@link MediaSessionCompat} to a {@link Player}. + *

+ * The connector listens for actions sent by the media session's controller and implements these + * actions by calling appropriate ExoPlayer methods. The playback state of the media session is + * automatically synced with the player. The connector can also be optionally extended by providing + * various collaborators: + *

+ */ +public final class MediaSessionConnector { + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession"); + } + + public static final String EXTRAS_PITCH = "EXO_PITCH"; + + /** + * Interface to which playback preparation actions are delegated. + */ + public interface PlaybackPreparer { + + long ACTIONS = PlaybackStateCompat.ACTION_PREPARE + | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH + | PlaybackStateCompat.ACTION_PREPARE_FROM_URI + | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + | PlaybackStateCompat.ACTION_PLAY_FROM_URI; + + /** + * Returns the actions which are supported by the preparer. The supported actions must be a + * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, + * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, + * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, + * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, + * {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, + * {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and + * {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI}. + * + * @return The bitmask of the supported media actions. + */ + long getSupportedPrepareActions(); + /** + * See {@link MediaSessionCompat.Callback#onPrepare()}. + */ + void onPrepare(); + /** + * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. + */ + void onPrepareFromMediaId(String mediaId, Bundle extras); + /** + * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. + */ + void onPrepareFromSearch(String query, Bundle extras); + /** + * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. + */ + void onPrepareFromUri(Uri uri, Bundle extras); + /** + * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. + */ + void onCommand(String command, Bundle extras, ResultReceiver cb); + } + + /** + * Interface to which playback actions are delegated. + */ + public interface PlaybackController { + + long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO + | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND + | PlaybackStateCompat.ACTION_STOP; + + /** + * Returns the actions which are supported by the controller. The supported actions must be a + * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, + * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, + * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, + * {@link PlaybackStateCompat#ACTION_REWIND} and {@link PlaybackStateCompat#ACTION_STOP}. + * + * @param player The player. + * @return The bitmask of the supported media actions. + */ + long getSupportedPlaybackActions(@Nullable Player player); + /** + * See {@link MediaSessionCompat.Callback#onPlay()}. + */ + void onPlay(Player player); + /** + * See {@link MediaSessionCompat.Callback#onPause()}. + */ + void onPause(Player player); + /** + * See {@link MediaSessionCompat.Callback#onSeekTo(long)}. + */ + void onSeekTo(Player player, long position); + /** + * See {@link MediaSessionCompat.Callback#onFastForward()}. + */ + void onFastForward(Player player); + /** + * See {@link MediaSessionCompat.Callback#onRewind()}. + */ + void onRewind(Player player); + /** + * See {@link MediaSessionCompat.Callback#onStop()}. + */ + void onStop(Player player); + } + + /** + * Handles queue navigation actions, and updates the media session queue by calling + * {@code MediaSessionCompat.setQueue()}. + */ + public interface QueueNavigator { + + long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; + + /** + * Returns the actions which are supported by the navigator. The supported actions must be a + * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, + * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, + * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}, + * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}. + * + * @param player The {@link Player}. + * @return The bitmask of the supported media actions. + */ + long getSupportedQueueNavigatorActions(@Nullable Player player); + /** + * Called when the timeline of the player has changed. + * + * @param player The player of which the timeline has changed. + */ + void onTimelineChanged(Player player); + /** + * Called when the current window index changed. + * + * @param player The player of which the current window index of the timeline has changed. + */ + void onCurrentWindowIndexChanged(Player player); + /** + * Gets the id of the currently active queue item, or + * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown. + *

+ * To let the connector publish metadata for the active queue item, the queue item with the + * returned id must be available in the list of items returned by + * {@link MediaControllerCompat#getQueue()}. + * + * @param player The player connected to the media session. + * @return The id of the active queue item. + */ + long getActiveQueueItemId(@Nullable Player player); + /** + * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. + */ + void onSkipToPrevious(Player player); + /** + * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. + */ + void onSkipToQueueItem(Player player, long id); + /** + * See {@link MediaSessionCompat.Callback#onSkipToNext()}. + */ + void onSkipToNext(Player player); + /** + * See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}. + */ + void onSetShuffleModeEnabled(Player player, boolean enabled); + } + + /** + * Handles media session queue edits. + */ + public interface QueueEditor { + + long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING; + + /** + * Returns {@link PlaybackStateCompat#ACTION_SET_RATING} or {@code 0}. The Media API does + * not declare action constants for adding and removing queue items. + * + * @param player The {@link Player}. + */ + long getSupportedQueueEditorActions(@Nullable Player player); + /** + * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}. + */ + void onAddQueueItem(Player player, MediaDescriptionCompat description); + /** + * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, + * int index)}. + */ + void onAddQueueItem(Player player, MediaDescriptionCompat description, int index); + /** + * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat + * description)}. + */ + void onRemoveQueueItem(Player player, MediaDescriptionCompat description); + /** + * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. + */ + void onRemoveQueueItemAt(Player player, int index); + /** + * See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. + */ + void onSetRating(Player player, RatingCompat rating); + } + + /** + * Provides a {@link PlaybackStateCompat.CustomAction} to be published and handles the action when + * sent by a media controller. + */ + public interface CustomActionProvider { + /** + * Called when a custom action provided by this provider is sent to the media session. + * + * @param action The name of the action which was sent by a media controller. + * @param extras Optional extras sent by a media controller. + */ + void onCustomAction(String action, Bundle extras); + + /** + * Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the + * media session by the connector or {@code null} if this action should not be published at the + * given player state. + * + * @return The custom action to be included in the session playback state or {@code null}. + */ + PlaybackStateCompat.CustomAction getCustomAction(); + } + + /** + * Converts an exception into an error code and a user readable error message. + */ + public interface ErrorMessageProvider { + /** + * Returns a pair consisting of an error code and a user readable error message for a given + * exception. + */ + Pair getErrorMessage(ExoPlaybackException playbackException); + } + + /** + * The wrapped {@link MediaSessionCompat}. + */ + public final MediaSessionCompat mediaSession; + + private final MediaControllerCompat mediaController; + private final Handler handler; + private final boolean doMaintainMetadata; + private final ExoPlayerEventListener exoPlayerEventListener; + private final MediaSessionCallback mediaSessionCallback; + private final PlaybackController playbackController; + + private Player player; + private CustomActionProvider[] customActionProviders; + private int currentWindowIndex; + private Map customActionMap; + private ErrorMessageProvider errorMessageProvider; + private PlaybackPreparer playbackPreparer; + private QueueNavigator queueNavigator; + private QueueEditor queueEditor; + private ExoPlaybackException playbackException; + + /** + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. + *

+ * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}. + * + * @param mediaSession The {@link MediaSessionCompat} to connect to. + */ + public MediaSessionConnector(MediaSessionCompat mediaSession) { + this(mediaSession, new DefaultPlaybackController()); + } + + /** + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. + *

+ * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. + * + * @param mediaSession The {@link MediaSessionCompat} to connect to. + * @param playbackController A {@link PlaybackController} for handling playback actions. + */ + public MediaSessionConnector(MediaSessionCompat mediaSession, + PlaybackController playbackController) { + this(mediaSession, playbackController, true); + } + + /** + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. + * + * @param mediaSession The {@link MediaSessionCompat} to connect to. + * @param playbackController A {@link PlaybackController} for handling playback actions. + * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If + * {@code false}, you need to maintain the metadata of the media session yourself (provide at + * least the duration to allow clients to show a progress bar). + */ + public MediaSessionConnector(MediaSessionCompat mediaSession, + PlaybackController playbackController, boolean doMaintainMetadata) { + this.mediaSession = mediaSession; + this.playbackController = playbackController; + this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() + : Looper.getMainLooper()); + this.doMaintainMetadata = doMaintainMetadata; + mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaController = mediaSession.getController(); + mediaSessionCallback = new MediaSessionCallback(); + exoPlayerEventListener = new ExoPlayerEventListener(); + customActionMap = Collections.emptyMap(); + } + + /** + * Sets the player to be connected to the media session. + *

+ * The order in which any {@link CustomActionProvider}s are passed determines the order of the + * actions published with the playback state of the session. + * + * @param player The player to be connected to the {@code MediaSession}. + * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player. + * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle + * custom actions. + */ + public void setPlayer(Player player, PlaybackPreparer playbackPreparer, + CustomActionProvider... customActionProviders) { + if (this.player != null) { + this.player.removeListener(exoPlayerEventListener); + mediaSession.setCallback(null); + } + this.playbackPreparer = playbackPreparer; + this.player = player; + this.customActionProviders = (player != null && customActionProviders != null) + ? customActionProviders : new CustomActionProvider[0]; + if (player != null) { + mediaSession.setCallback(mediaSessionCallback, handler); + player.addListener(exoPlayerEventListener); + } + updateMediaSessionPlaybackState(); + updateMediaSessionMetadata(); + } + + /** + * Sets the {@link ErrorMessageProvider}. + * + * @param errorMessageProvider The error message provider. + */ + public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) { + this.errorMessageProvider = errorMessageProvider; + } + + /** + * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT}, + * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and + * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. + * + * @param queueNavigator The queue navigator. + */ + public void setQueueNavigator(QueueNavigator queueNavigator) { + this.queueNavigator = queueNavigator; + } + + /** + * Sets the {@link QueueEditor} to handle queue edits sent by the media controller. + * + * @param queueEditor The queue editor. + */ + public void setQueueEditor(QueueEditor queueEditor) { + this.queueEditor = queueEditor; + } + + private void updateMediaSessionPlaybackState() { + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); + if (player == null) { + builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); + mediaSession.setPlaybackState(builder.build()); + return; + } + + Map currentActions = new HashMap<>(); + for (CustomActionProvider customActionProvider : customActionProviders) { + PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(); + if (customAction != null) { + currentActions.put(customAction.getAction(), customActionProvider); + builder.addCustomAction(customAction); + } + } + customActionMap = Collections.unmodifiableMap(currentActions); + + int sessionPlaybackState = playbackException != null ? PlaybackStateCompat.STATE_ERROR + : mapPlaybackState(player.getPlaybackState(), player.getPlayWhenReady()); + if (playbackException != null) { + if (errorMessageProvider != null) { + Pair message = errorMessageProvider.getErrorMessage(playbackException); + builder.setErrorMessage(message.first, message.second); + } + if (player.getPlaybackState() != Player.STATE_IDLE) { + playbackException = null; + } + } + long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) + : MediaSessionCompat.QueueItem.UNKNOWN_ID; + Bundle extras = new Bundle(); + extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch); + builder.setActions(buildPlaybackActions()) + .setActiveQueueItemId(activeQueueItemId) + .setBufferedPosition(player.getBufferedPosition()) + .setState(sessionPlaybackState, player.getCurrentPosition(), + player.getPlaybackParameters().speed, SystemClock.elapsedRealtime()) + .setExtras(extras); + mediaSession.setPlaybackState(builder.build()); + } + + private long buildPlaybackActions() { + long actions = 0; + if (playbackController != null) { + actions |= (PlaybackController.ACTIONS & playbackController + .getSupportedPlaybackActions(player)); + } + if (playbackPreparer != null) { + actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions()); + } + if (queueNavigator != null) { + actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions( + player)); + } + if (queueEditor != null) { + actions |= (QueueEditor.ACTIONS & queueEditor.getSupportedQueueEditorActions(player)); + } + return actions; + } + + private void updateMediaSessionMetadata() { + if (doMaintainMetadata) { + MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); + if (player != null && player.isPlayingAd()) { + builder.putLong(MediaMetadataCompat.METADATA_KEY_ADVERTISEMENT, 1); + } + builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, player == null ? 0 + : player.getDuration() == C.TIME_UNSET ? -1 : player.getDuration()); + + if (queueNavigator != null) { + long activeQueueItemId = queueNavigator.getActiveQueueItemId(player); + List queue = mediaController.getQueue(); + for (int i = 0; queue != null && i < queue.size(); i++) { + MediaSessionCompat.QueueItem queueItem = queue.get(i); + if (queueItem.getQueueId() == activeQueueItemId) { + MediaDescriptionCompat description = queueItem.getDescription(); + if (description.getTitle() != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, + String.valueOf(description.getTitle())); + } + if (description.getSubtitle() != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, + String.valueOf(description.getSubtitle())); + } + if (description.getDescription() != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, + String.valueOf(description.getDescription())); + } + if (description.getIconBitmap() != null) { + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, + description.getIconBitmap()); + } + if (description.getIconUri() != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, + String.valueOf(description.getIconUri())); + } + if (description.getMediaId() != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, + String.valueOf(description.getMediaId())); + } + if (description.getMediaUri() != null) { + builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, + String.valueOf(description.getMediaUri())); + } + break; + } + } + } + mediaSession.setMetadata(builder.build()); + } + } + + private int mapPlaybackState(int exoPlayerPlaybackState, boolean playWhenReady) { + switch (exoPlayerPlaybackState) { + case Player.STATE_BUFFERING: + return PlaybackStateCompat.STATE_BUFFERING; + case Player.STATE_READY: + return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; + case Player.STATE_ENDED: + return PlaybackStateCompat.STATE_PAUSED; + default: + return PlaybackStateCompat.STATE_NONE; + } + } + + private boolean canDispatchToPlaybackPreparer(long action) { + return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions() + & PlaybackPreparer.ACTIONS & action) != 0; + } + + private boolean canDispatchToPlaybackController(long action) { + return playbackController != null && (playbackController.getSupportedPlaybackActions(player) + & PlaybackController.ACTIONS & action) != 0; + } + + private boolean canDispatchToQueueNavigator(long action) { + return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player) + & QueueNavigator.ACTIONS & action) != 0; + } + + private boolean canDispatchToQueueEditor(long action) { + return queueEditor != null && (queueEditor.getSupportedQueueEditorActions(player) + & QueueEditor.ACTIONS & action) != 0; + } + + private class ExoPlayerEventListener implements Player.EventListener { + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + if (queueNavigator != null) { + queueNavigator.onTimelineChanged(player); + } + currentWindowIndex = player.getCurrentWindowIndex(); + updateMediaSessionMetadata(); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updateMediaSessionPlaybackState(); + } + + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + mediaSession.setRepeatMode(repeatMode == Player.REPEAT_MODE_ONE + ? PlaybackStateCompat.REPEAT_MODE_ONE : repeatMode == Player.REPEAT_MODE_ALL + ? PlaybackStateCompat.REPEAT_MODE_ALL : PlaybackStateCompat.REPEAT_MODE_NONE); + updateMediaSessionPlaybackState(); + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + playbackException = error; + updateMediaSessionPlaybackState(); + } + + @Override + public void onPositionDiscontinuity() { + if (currentWindowIndex != player.getCurrentWindowIndex()) { + if (queueNavigator != null) { + queueNavigator.onCurrentWindowIndexChanged(player); + } + updateMediaSessionMetadata(); + currentWindowIndex = player.getCurrentWindowIndex(); + } + updateMediaSessionPlaybackState(); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + updateMediaSessionPlaybackState(); + } + + } + + private class MediaSessionCallback extends MediaSessionCompat.Callback { + + @Override + public void onPlay() { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PLAY)) { + playbackController.onPlay(player); + } + } + + @Override + public void onPause() { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PAUSE)) { + playbackController.onPause(player); + } + } + + @Override + public void onSeekTo(long position) { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SEEK_TO)) { + playbackController.onSeekTo(player, position); + } + } + + @Override + public void onFastForward() { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_FAST_FORWARD)) { + playbackController.onFastForward(player); + } + } + + @Override + public void onRewind() { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_REWIND)) { + playbackController.onRewind(player); + } + } + + @Override + public void onStop() { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_STOP)) { + playbackController.onStop(player); + } + } + + @Override + public void onSkipToNext() { + if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) { + queueNavigator.onSkipToNext(player); + } + } + + @Override + public void onSkipToPrevious() { + if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) { + queueNavigator.onSkipToPrevious(player); + } + } + + @Override + public void onSkipToQueueItem(long id) { + if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM)) { + queueNavigator.onSkipToQueueItem(player, id); + } + } + + @Override + public void onSetRepeatMode(int repeatMode) { + // implemented as custom action + } + + @Override + public void onCustomAction(@NonNull String action, @Nullable Bundle extras) { + Map actionMap = customActionMap; + if (actionMap.containsKey(action)) { + actionMap.get(action).onCustomAction(action, extras); + updateMediaSessionPlaybackState(); + } + } + + @Override + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + if (playbackPreparer != null) { + playbackPreparer.onCommand(command, extras, cb); + } + } + + @Override + public void onPrepare() { + if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { + player.stop(); + player.setPlayWhenReady(false); + playbackPreparer.onPrepare(); + } + } + + @Override + public void onPrepareFromMediaId(String mediaId, Bundle extras) { + if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { + player.stop(); + player.setPlayWhenReady(false); + playbackPreparer.onPrepareFromMediaId(mediaId, extras); + } + } + + @Override + public void onPrepareFromSearch(String query, Bundle extras) { + if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { + player.stop(); + player.setPlayWhenReady(false); + playbackPreparer.onPrepareFromSearch(query, extras); + } + } + + @Override + public void onPrepareFromUri(Uri uri, Bundle extras) { + if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { + player.stop(); + player.setPlayWhenReady(false); + playbackPreparer.onPrepareFromUri(uri, extras); + } + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { + player.stop(); + player.setPlayWhenReady(true); + playbackPreparer.onPrepareFromMediaId(mediaId, extras); + } + } + + @Override + public void onPlayFromSearch(String query, Bundle extras) { + if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { + player.stop(); + player.setPlayWhenReady(true); + playbackPreparer.onPrepareFromSearch(query, extras); + } + } + + @Override + public void onPlayFromUri(Uri uri, Bundle extras) { + if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { + player.stop(); + player.setPlayWhenReady(true); + playbackPreparer.onPrepareFromUri(uri, extras); + } + } + + @Override + public void onSetShuffleModeEnabled(boolean enabled) { + if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { + queueNavigator.onSetShuffleModeEnabled(player, enabled); + } + } + + @Override + public void onAddQueueItem(MediaDescriptionCompat description) { + if (queueEditor != null) { + queueEditor.onAddQueueItem(player, description); + } + } + + @Override + public void onAddQueueItem(MediaDescriptionCompat description, int index) { + if (queueEditor != null) { + queueEditor.onAddQueueItem(player, description, index); + } + } + + @Override + public void onRemoveQueueItem(MediaDescriptionCompat description) { + if (queueEditor != null) { + queueEditor.onRemoveQueueItem(player, description); + } + } + + @Override + public void onRemoveQueueItemAt(int index) { + if (queueEditor != null) { + queueEditor.onRemoveQueueItemAt(player, index); + } + } + + @Override + public void onSetRating(RatingCompat rating) { + if (canDispatchToQueueEditor(PlaybackStateCompat.ACTION_SET_RATING)) { + queueEditor.onSetRating(player, rating); + } + } + + } + +} diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java new file mode 100644 index 0000000000..abefe533ce --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -0,0 +1,106 @@ +package com.google.android.exoplayer2.ext.mediasession; +/* + * Copyright (c) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.media.session.PlaybackStateCompat; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.util.RepeatModeUtil; + +/** + * Provides a custom action for toggling repeat modes. + */ +public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider { + + /** + * The default repeat toggle modes. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; + + private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE"; + + private final Player player; + @RepeatModeUtil.RepeatToggleModes + private final int repeatToggleModes; + private final CharSequence repeatAllDescription; + private final CharSequence repeatOneDescription; + private final CharSequence repeatOffDescription; + + /** + * Creates a new instance. + *

+ * Equivalent to {@code RepeatModeActionProvider(context, player, + * RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}. + * + * @param context The context. + * @param player The player on which to toggle the repeat mode. + */ + public RepeatModeActionProvider(Context context, Player player) { + this(context, player, DEFAULT_REPEAT_TOGGLE_MODES); + } + + /** + * Creates a new instance enabling the given repeat toggle modes. + * + * @param context The context. + * @param player The player on which to toggle the repeat mode. + * @param repeatToggleModes The toggle modes to enable. + */ + public RepeatModeActionProvider(Context context, Player player, + @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + this.player = player; + this.repeatToggleModes = repeatToggleModes; + repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description); + repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description); + repeatOffDescription = context.getString(R.string.exo_media_action_repeat_off_description); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + int mode = player.getRepeatMode(); + int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes); + if (mode != proposedMode) { + player.setRepeatMode(proposedMode); + } + } + + @Override + public PlaybackStateCompat.CustomAction getCustomAction() { + CharSequence actionLabel; + int iconResourceId; + switch (player.getRepeatMode()) { + case Player.REPEAT_MODE_ONE: + actionLabel = repeatOneDescription; + iconResourceId = R.drawable.exo_media_action_repeat_one; + break; + case Player.REPEAT_MODE_ALL: + actionLabel = repeatAllDescription; + iconResourceId = R.drawable.exo_media_action_repeat_all; + break; + case Player.REPEAT_MODE_OFF: + default: + actionLabel = repeatOffDescription; + iconResourceId = R.drawable.exo_media_action_repeat_off; + break; + } + PlaybackStateCompat.CustomAction.Builder repeatBuilder = new PlaybackStateCompat.CustomAction + .Builder(ACTION_REPEAT_MODE, actionLabel, iconResourceId); + return repeatBuilder.build(); + } + +} diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java new file mode 100644 index 0000000000..521b4cd6e3 --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.mediasession; + +import android.support.annotation.Nullable; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the + * windows of a {@link Player}'s {@link Timeline} to the media session queue. + */ +public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator { + + public static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; + public static final int DEFAULT_MAX_QUEUE_SIZE = 10; + + private final MediaSessionCompat mediaSession; + protected final int maxQueueSize; + + private long activeQueueItemId; + + /** + * Creates an instance for a given {@link MediaSessionCompat}. + *

+ * Equivalent to {@code TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}. + * + * @param mediaSession The {@link MediaSessionCompat}. + */ + public TimelineQueueNavigator(MediaSessionCompat mediaSession) { + this(mediaSession, DEFAULT_MAX_QUEUE_SIZE); + } + + /** + * Creates an instance for a given {@link MediaSessionCompat} and maximum queue size. + *

+ * If the number of windows in the {@link Player}'s {@link Timeline} exceeds {@code maxQueueSize}, + * the media session queue will correspond to {@code maxQueueSize} windows centered on the one + * currently being played. + * + * @param mediaSession The {@link MediaSessionCompat}. + * @param maxQueueSize The maximum queue size. + */ + public TimelineQueueNavigator(MediaSessionCompat mediaSession, int maxQueueSize) { + this.mediaSession = mediaSession; + this.maxQueueSize = maxQueueSize; + activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + } + + /** + * Gets the {@link MediaDescriptionCompat} for a given timeline window index. + * + * @param windowIndex The timeline window index for which to provide a description. + * @return A {@link MediaDescriptionCompat}. + */ + public abstract MediaDescriptionCompat getMediaDescription(int windowIndex); + + @Override + public long getSupportedQueueNavigatorActions(Player player) { + if (player == null || player.getCurrentTimeline().getWindowCount() < 2) { + return 0; + } + if (player.getRepeatMode() != Player.REPEAT_MODE_OFF) { + return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + } + + int currentWindowIndex = player.getCurrentWindowIndex(); + long actions; + if (currentWindowIndex == 0) { + actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + } else if (currentWindowIndex == player.getCurrentTimeline().getWindowCount() - 1) { + actions = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + } else { + actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + } + return actions | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + } + + @Override + public final void onTimelineChanged(Player player) { + publishFloatingQueueWindow(player); + } + + @Override + public final void onCurrentWindowIndexChanged(Player player) { + if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID + || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { + publishFloatingQueueWindow(player); + } else if (!player.getCurrentTimeline().isEmpty()) { + activeQueueItemId = player.getCurrentWindowIndex(); + } + } + + @Override + public final long getActiveQueueItemId(@Nullable Player player) { + return activeQueueItemId; + } + + @Override + public void onSkipToPrevious(Player player) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(), + player.getRepeatMode()); + if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || previousWindowIndex == C.INDEX_UNSET) { + player.seekTo(0); + } else { + player.seekTo(previousWindowIndex, C.TIME_UNSET); + } + } + + @Override + public void onSkipToQueueItem(Player player, long id) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int windowIndex = (int) id; + if (0 <= windowIndex && windowIndex < timeline.getWindowCount()) { + player.seekTo(windowIndex, C.TIME_UNSET); + } + } + + @Override + public void onSkipToNext(Player player) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(), + player.getRepeatMode()); + if (nextWindowIndex != C.INDEX_UNSET) { + player.seekTo(nextWindowIndex, C.TIME_UNSET); + } + } + + @Override + public void onSetShuffleModeEnabled(Player player, boolean enabled) { + // TODO: Implement this. + } + + private void publishFloatingQueueWindow(Player player) { + if (player.getCurrentTimeline().isEmpty()) { + mediaSession.setQueue(Collections.emptyList()); + activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + return; + } + int windowCount = player.getCurrentTimeline().getWindowCount(); + int currentWindowIndex = player.getCurrentWindowIndex(); + int queueSize = Math.min(maxQueueSize, windowCount); + int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, + windowCount - queueSize); + List queue = new ArrayList<>(); + for (int i = startIndex; i < startIndex + queueSize; i++) { + queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i)); + } + mediaSession.setQueue(queue); + activeQueueItemId = currentWindowIndex; + } + +} diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml new file mode 100644 index 0000000000..dad37fa1f0 --- /dev/null +++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_all.xml @@ -0,0 +1,23 @@ + + + + diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml new file mode 100644 index 0000000000..132eae0d76 --- /dev/null +++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_off.xml @@ -0,0 +1,23 @@ + + + + diff --git a/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml new file mode 100644 index 0000000000..d51010566a --- /dev/null +++ b/extensions/mediasession/src/main/res/drawable-anydpi-v21/exo_media_action_repeat_one.xml @@ -0,0 +1,23 @@ + + + + diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png new file mode 100644 index 0000000000..2824e7847c Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_all.png differ diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png new file mode 100644 index 0000000000..0b92f583da Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_off.png differ diff --git a/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png new file mode 100644 index 0000000000..232aa2b1cd Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-hdpi/exo_media_action_repeat_one.png differ diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png new file mode 100644 index 0000000000..5c91a47519 Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_all.png differ diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png new file mode 100644 index 0000000000..a94abd864f Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_off.png differ diff --git a/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png new file mode 100644 index 0000000000..a59a985239 Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-ldpi/exo_media_action_repeat_one.png differ diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png new file mode 100644 index 0000000000..97f7e1cc75 Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_all.png differ diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png new file mode 100644 index 0000000000..6a02321702 Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_off.png differ diff --git a/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png new file mode 100644 index 0000000000..59bac33705 Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-mdpi/exo_media_action_repeat_one.png differ diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png new file mode 100644 index 0000000000..2baaedecbf Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_all.png differ diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png new file mode 100644 index 0000000000..2468f92f9f Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_off.png differ diff --git a/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png new file mode 100644 index 0000000000..4e1d53db77 Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xhdpi/exo_media_action_repeat_one.png differ diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png new file mode 100644 index 0000000000..d7207ebc0d Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_all.png differ diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png new file mode 100644 index 0000000000..4d6253ead6 Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_off.png differ diff --git a/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png new file mode 100644 index 0000000000..d577f4ebcd Binary files /dev/null and b/extensions/mediasession/src/main/res/drawable-xxhdpi/exo_media_action_repeat_one.png differ diff --git a/extensions/mediasession/src/main/res/values-af/strings.xml b/extensions/mediasession/src/main/res/values-af/strings.xml new file mode 100644 index 0000000000..4ef78cd84f --- /dev/null +++ b/extensions/mediasession/src/main/res/values-af/strings.xml @@ -0,0 +1,21 @@ + + + + "Herhaal alles" + "Herhaal niks" + "Herhaal een" + diff --git a/extensions/mediasession/src/main/res/values-am/strings.xml b/extensions/mediasession/src/main/res/values-am/strings.xml new file mode 100644 index 0000000000..531f605584 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-am/strings.xml @@ -0,0 +1,21 @@ + + + + "ሁሉንም ድገም" + "ምንም አትድገም" + "አንዱን ድገም" + diff --git a/extensions/mediasession/src/main/res/values-ar/strings.xml b/extensions/mediasession/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..0101a746e0 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ar/strings.xml @@ -0,0 +1,21 @@ + + + + "تكرار الكل" + "عدم التكرار" + "تكرار مقطع واحد" + diff --git a/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml new file mode 100644 index 0000000000..34408143fa --- /dev/null +++ b/extensions/mediasession/src/main/res/values-az-rAZ/strings.xml @@ -0,0 +1,21 @@ + + + + "Bütün təkrarlayın" + "Təkrar bir" + "Heç bir təkrar" + diff --git a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml new file mode 100644 index 0000000000..67a51cf85e --- /dev/null +++ b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml @@ -0,0 +1,21 @@ + + + + "Ponovi sve" + "Ne ponavljaj nijednu" + "Ponovi jednu" + diff --git a/extensions/mediasession/src/main/res/values-be-rBY/strings.xml b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml new file mode 100644 index 0000000000..2f05607235 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-be-rBY/strings.xml @@ -0,0 +1,21 @@ + + + + "Паўтарыць усё" + "Паўтараць ні" + "Паўтарыць адзін" + diff --git a/extensions/mediasession/src/main/res/values-bg/strings.xml b/extensions/mediasession/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..16910d640a --- /dev/null +++ b/extensions/mediasession/src/main/res/values-bg/strings.xml @@ -0,0 +1,21 @@ + + + + "Повтаряне на всички" + "Без повтаряне" + "Повтаряне на един елемент" + diff --git a/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml new file mode 100644 index 0000000000..8872b464c6 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-bn-rBD/strings.xml @@ -0,0 +1,21 @@ + + + + "সবগুলির পুনরাবৃত্তি করুন" + "একটিরও পুনরাবৃত্তি করবেন না" + "একটির পুনরাবৃত্তি করুন" + diff --git a/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml new file mode 100644 index 0000000000..d0bf068573 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-bs-rBA/strings.xml @@ -0,0 +1,21 @@ + + + + "Ponovite sve" + "Ne ponavljaju" + "Ponovite jedan" + diff --git a/extensions/mediasession/src/main/res/values-ca/strings.xml b/extensions/mediasession/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..89414d736e --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ca/strings.xml @@ -0,0 +1,21 @@ + + + + "Repeteix-ho tot" + "No en repeteixis cap" + "Repeteix-ne un" + diff --git a/extensions/mediasession/src/main/res/values-cs/strings.xml b/extensions/mediasession/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..784d872570 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-cs/strings.xml @@ -0,0 +1,21 @@ + + + + "Opakovat vše" + "Neopakovat" + "Opakovat jednu položku" + diff --git a/extensions/mediasession/src/main/res/values-da/strings.xml b/extensions/mediasession/src/main/res/values-da/strings.xml new file mode 100644 index 0000000000..2c9784d122 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-da/strings.xml @@ -0,0 +1,21 @@ + + + + "Gentag alle" + "Gentag ingen" + "Gentag en" + diff --git a/extensions/mediasession/src/main/res/values-de/strings.xml b/extensions/mediasession/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..c11e449665 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-de/strings.xml @@ -0,0 +1,21 @@ + + + + "Alle wiederholen" + "Keinen Titel wiederholen" + "Einen Titel wiederholen" + diff --git a/extensions/mediasession/src/main/res/values-el/strings.xml b/extensions/mediasession/src/main/res/values-el/strings.xml new file mode 100644 index 0000000000..6279af5d64 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-el/strings.xml @@ -0,0 +1,21 @@ + + + + "Επανάληψη όλων" + "Καμία επανάληψη" + "Επανάληψη ενός στοιχείου" + diff --git a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml new file mode 100644 index 0000000000..a3fccf8b52 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml @@ -0,0 +1,21 @@ + + + + "Repeat all" + "Repeat none" + "Repeat one" + diff --git a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..a3fccf8b52 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,21 @@ + + + + "Repeat all" + "Repeat none" + "Repeat one" + diff --git a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml new file mode 100644 index 0000000000..a3fccf8b52 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "Repeat all" + "Repeat none" + "Repeat one" + diff --git a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml new file mode 100644 index 0000000000..0fe29d3d5a --- /dev/null +++ b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml @@ -0,0 +1,21 @@ + + + + "Repetir todo" + "No repetir" + "Repetir uno" + diff --git a/extensions/mediasession/src/main/res/values-es/strings.xml b/extensions/mediasession/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..0fe29d3d5a --- /dev/null +++ b/extensions/mediasession/src/main/res/values-es/strings.xml @@ -0,0 +1,21 @@ + + + + "Repetir todo" + "No repetir" + "Repetir uno" + diff --git a/extensions/mediasession/src/main/res/values-et-rEE/strings.xml b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml new file mode 100644 index 0000000000..1bc3b59706 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-et-rEE/strings.xml @@ -0,0 +1,21 @@ + + + + "Korda kõike" + "Ära korda midagi" + "Korda ühte" + diff --git a/extensions/mediasession/src/main/res/values-eu-rES/strings.xml b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml new file mode 100644 index 0000000000..f15f03160f --- /dev/null +++ b/extensions/mediasession/src/main/res/values-eu-rES/strings.xml @@ -0,0 +1,21 @@ + + + + "Errepikatu guztiak" + "Ez errepikatu" + "Errepikatu bat" + diff --git a/extensions/mediasession/src/main/res/values-fa/strings.xml b/extensions/mediasession/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000000..e37a08de64 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-fa/strings.xml @@ -0,0 +1,21 @@ + + + + "تکرار همه" + "تکرار هیچ‌کدام" + "یک‌بار تکرار" + diff --git a/extensions/mediasession/src/main/res/values-fi/strings.xml b/extensions/mediasession/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..c920827976 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-fi/strings.xml @@ -0,0 +1,21 @@ + + + + "Toista kaikki" + "Toista ei mitään" + "Toista yksi" + diff --git a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml new file mode 100644 index 0000000000..c5191e74a9 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml @@ -0,0 +1,21 @@ + + + + "Tout lire en boucle" + "Aucune répétition" + "Répéter un élément" + diff --git a/extensions/mediasession/src/main/res/values-fr/strings.xml b/extensions/mediasession/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..1d76358d1f --- /dev/null +++ b/extensions/mediasession/src/main/res/values-fr/strings.xml @@ -0,0 +1,21 @@ + + + + "Tout lire en boucle" + "Ne rien lire en boucle" + "Lire en boucle un élément" + diff --git a/extensions/mediasession/src/main/res/values-gl-rES/strings.xml b/extensions/mediasession/src/main/res/values-gl-rES/strings.xml new file mode 100644 index 0000000000..6b65b3e843 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-gl-rES/strings.xml @@ -0,0 +1,21 @@ + + + + "Repetir todo" + "Non repetir" + "Repetir un" + diff --git a/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml new file mode 100644 index 0000000000..0eb9cab37e --- /dev/null +++ b/extensions/mediasession/src/main/res/values-gu-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "બધા પુનરાવર્તન કરો" + "કંઈ પુનરાવર્તન કરો" + "એક પુનરાવર્તન કરો" + diff --git a/extensions/mediasession/src/main/res/values-hi/strings.xml b/extensions/mediasession/src/main/res/values-hi/strings.xml new file mode 100644 index 0000000000..8ce336d5e5 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-hi/strings.xml @@ -0,0 +1,21 @@ + + + + "सभी को दोहराएं" + "कुछ भी न दोहराएं" + "एक दोहराएं" + diff --git a/extensions/mediasession/src/main/res/values-hr/strings.xml b/extensions/mediasession/src/main/res/values-hr/strings.xml new file mode 100644 index 0000000000..9f995ec15b --- /dev/null +++ b/extensions/mediasession/src/main/res/values-hr/strings.xml @@ -0,0 +1,21 @@ + + + + "Ponovi sve" + "Bez ponavljanja" + "Ponovi jedno" + diff --git a/extensions/mediasession/src/main/res/values-hu/strings.xml b/extensions/mediasession/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..2335ade72e --- /dev/null +++ b/extensions/mediasession/src/main/res/values-hu/strings.xml @@ -0,0 +1,21 @@ + + + + "Összes ismétlése" + "Nincs ismétlés" + "Egy ismétlése" + diff --git a/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml new file mode 100644 index 0000000000..19a89e6c87 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-hy-rAM/strings.xml @@ -0,0 +1,21 @@ + + + + "կրկնել այն ամենը" + "Չկրկնել" + "Կրկնել մեկը" + diff --git a/extensions/mediasession/src/main/res/values-in/strings.xml b/extensions/mediasession/src/main/res/values-in/strings.xml new file mode 100644 index 0000000000..093a7f8576 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-in/strings.xml @@ -0,0 +1,21 @@ + + + + "Ulangi Semua" + "Jangan Ulangi" + "Ulangi Satu" + diff --git a/extensions/mediasession/src/main/res/values-is-rIS/strings.xml b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml new file mode 100644 index 0000000000..b200abbdb2 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-is-rIS/strings.xml @@ -0,0 +1,21 @@ + + + + "Endurtaka allt" + "Endurtaka ekkert" + "Endurtaka eitt" + diff --git a/extensions/mediasession/src/main/res/values-it/strings.xml b/extensions/mediasession/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..c0682519f9 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-it/strings.xml @@ -0,0 +1,21 @@ + + + + "Ripeti tutti" + "Non ripetere nessuno" + "Ripeti uno" + diff --git a/extensions/mediasession/src/main/res/values-iw/strings.xml b/extensions/mediasession/src/main/res/values-iw/strings.xml new file mode 100644 index 0000000000..5cf23d5a4c --- /dev/null +++ b/extensions/mediasession/src/main/res/values-iw/strings.xml @@ -0,0 +1,21 @@ + + + + "חזור על הכל" + "אל תחזור על כלום" + "חזור על פריט אחד" + diff --git a/extensions/mediasession/src/main/res/values-ja/strings.xml b/extensions/mediasession/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..6f543fbdee --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ja/strings.xml @@ -0,0 +1,21 @@ + + + + "全曲を繰り返し" + "繰り返しなし" + "1曲を繰り返し" + diff --git a/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml new file mode 100644 index 0000000000..96656612a7 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ka-rGE/strings.xml @@ -0,0 +1,21 @@ + + + + "გამეორება ყველა" + "გაიმეორეთ არცერთი" + "გაიმეორეთ ერთი" + diff --git a/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml new file mode 100644 index 0000000000..be4140120d --- /dev/null +++ b/extensions/mediasession/src/main/res/values-kk-rKZ/strings.xml @@ -0,0 +1,21 @@ + + + + "Барлығын қайталау" + "Ешқайсысын қайталамау" + "Біреуін қайталау" + diff --git a/extensions/mediasession/src/main/res/values-km-rKH/strings.xml b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml new file mode 100644 index 0000000000..dd4b734e30 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-km-rKH/strings.xml @@ -0,0 +1,21 @@ + + + + "ធ្វើ​ម្ដង​ទៀត​ទាំងអស់" + "មិន​ធ្វើ​ឡើង​វិញ" + "ធ្វើ​​ឡើងវិញ​ម្ដង" + diff --git a/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml new file mode 100644 index 0000000000..3d79aca9e2 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-kn-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ" + "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ" + "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ" + diff --git a/extensions/mediasession/src/main/res/values-ko/strings.xml b/extensions/mediasession/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000000..d269937771 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ko/strings.xml @@ -0,0 +1,21 @@ + + + + "전체 반복" + "반복 안함" + "한 항목 반복" + diff --git a/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml new file mode 100644 index 0000000000..a8978ecc61 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ky-rKG/strings.xml @@ -0,0 +1,21 @@ + + + + "Баарын кайталоо" + "Эч бирин кайталабоо" + "Бирөөнү кайталоо" + diff --git a/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml new file mode 100644 index 0000000000..950a9ba097 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-lo-rLA/strings.xml @@ -0,0 +1,21 @@ + + + + "ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ" + "​ບໍ່ຫຼິ້ນ​ຊ້ຳ" + "ຫຼິ້ນ​ຊ້ຳ" + diff --git a/extensions/mediasession/src/main/res/values-lt/strings.xml b/extensions/mediasession/src/main/res/values-lt/strings.xml new file mode 100644 index 0000000000..ae8f1cf8c3 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-lt/strings.xml @@ -0,0 +1,21 @@ + + + + "Kartoti viską" + "Nekartoti nieko" + "Kartoti vieną" + diff --git a/extensions/mediasession/src/main/res/values-lv/strings.xml b/extensions/mediasession/src/main/res/values-lv/strings.xml new file mode 100644 index 0000000000..a69f6a0ad5 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-lv/strings.xml @@ -0,0 +1,21 @@ + + + + "Atkārtot visu" + "Neatkārtot nevienu" + "Atkārtot vienu" + diff --git a/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml new file mode 100644 index 0000000000..ddf2a60c20 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-mk-rMK/strings.xml @@ -0,0 +1,21 @@ + + + + "Повтори ги сите" + "Не повторувај ниту една" + "Повтори една" + diff --git a/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml new file mode 100644 index 0000000000..6f869e2931 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ml-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "എല്ലാം ആവർത്തിക്കുക" + "ഒന്നും ആവർത്തിക്കരുത്" + "ഒന്ന് ആവർത്തിക്കുക" + diff --git a/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml new file mode 100644 index 0000000000..8d3074b91a --- /dev/null +++ b/extensions/mediasession/src/main/res/values-mn-rMN/strings.xml @@ -0,0 +1,21 @@ + + + + "Бүгдийг давтах" + "Алийг нь ч давтахгүй" + "Нэгийг давтах" + diff --git a/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml new file mode 100644 index 0000000000..6e4bfccc16 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-mr-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "सर्व पुनरावृत्ती करा" + "काहीही पुनरावृत्ती करू नका" + "एक पुनरावृत्ती करा" + diff --git a/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml b/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml new file mode 100644 index 0000000000..829542b668 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ms-rMY/strings.xml @@ -0,0 +1,21 @@ + + + + "Ulang semua" + "Tiada ulangan" + "Ulangan" + diff --git a/extensions/mediasession/src/main/res/values-my-rMM/strings.xml b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml new file mode 100644 index 0000000000..aeb1375ebf --- /dev/null +++ b/extensions/mediasession/src/main/res/values-my-rMM/strings.xml @@ -0,0 +1,21 @@ + + + + "အားလုံး ထပ်တလဲလဲဖွင့်ရန်" + "ထပ်တလဲလဲမဖွင့်ရန်" + "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်" + diff --git a/extensions/mediasession/src/main/res/values-nb/strings.xml b/extensions/mediasession/src/main/res/values-nb/strings.xml new file mode 100644 index 0000000000..10f334b226 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-nb/strings.xml @@ -0,0 +1,21 @@ + + + + "Gjenta alle" + "Ikke gjenta noen" + "Gjenta én" + diff --git a/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml new file mode 100644 index 0000000000..6d81ce5684 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ne-rNP/strings.xml @@ -0,0 +1,21 @@ + + + + "सबै दोहोर्याउनुहोस्" + "कुनै पनि नदोहोर्याउनुहोस्" + "एउटा दोहोर्याउनुहोस्" + diff --git a/extensions/mediasession/src/main/res/values-nl/strings.xml b/extensions/mediasession/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..55997be098 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-nl/strings.xml @@ -0,0 +1,21 @@ + + + + "Alles herhalen" + "Niet herhalen" + "Eén herhalen" + diff --git a/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml new file mode 100644 index 0000000000..8eee0bee16 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-pa-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "ਸਭ ਨੂੰ ਦੁਹਰਾਓ" + "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ" + "ਇੱਕ ਦੁਹਰਾਓ" + diff --git a/extensions/mediasession/src/main/res/values-pl/strings.xml b/extensions/mediasession/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000000..6a52d58b63 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-pl/strings.xml @@ -0,0 +1,21 @@ + + + + "Powtórz wszystkie" + "Nie powtarzaj" + "Powtórz jeden" + diff --git a/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..efb8fc433f --- /dev/null +++ b/extensions/mediasession/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,21 @@ + + + + "Repetir tudo" + "Não repetir" + "Repetir um" + diff --git a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..efb8fc433f --- /dev/null +++ b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,21 @@ + + + + "Repetir tudo" + "Não repetir" + "Repetir um" + diff --git a/extensions/mediasession/src/main/res/values-pt/strings.xml b/extensions/mediasession/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000000..aadebbb3b0 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-pt/strings.xml @@ -0,0 +1,21 @@ + + + + "Repetir tudo" + "Não repetir" + "Repetir uma" + diff --git a/extensions/mediasession/src/main/res/values-ro/strings.xml b/extensions/mediasession/src/main/res/values-ro/strings.xml new file mode 100644 index 0000000000..f6aee447e5 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ro/strings.xml @@ -0,0 +1,21 @@ + + + + "Repetați toate" + "Repetați niciuna" + "Repetați unul" + diff --git a/extensions/mediasession/src/main/res/values-ru/strings.xml b/extensions/mediasession/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..575ad9f930 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ru/strings.xml @@ -0,0 +1,21 @@ + + + + "Повторять все" + "Не повторять" + "Повторять один элемент" + diff --git a/extensions/mediasession/src/main/res/values-si-rLK/strings.xml b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml new file mode 100644 index 0000000000..8e172ac268 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-si-rLK/strings.xml @@ -0,0 +1,21 @@ + + + + "සියලු නැවත" + "කිසිවක් නැවත" + "නැවත නැවත එක්" + diff --git a/extensions/mediasession/src/main/res/values-sk/strings.xml b/extensions/mediasession/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..5d092003e5 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-sk/strings.xml @@ -0,0 +1,21 @@ + + + + "Opakovať všetko" + "Neopakovať" + "Opakovať jednu položku" + diff --git a/extensions/mediasession/src/main/res/values-sl/strings.xml b/extensions/mediasession/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000000..ecac3800c8 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-sl/strings.xml @@ -0,0 +1,21 @@ + + + + "Ponovi vse" + "Ne ponovi" + "Ponovi eno" + diff --git a/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml new file mode 100644 index 0000000000..6da24cc4c7 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-sq-rAL/strings.xml @@ -0,0 +1,21 @@ + + + + "Përsërit të gjithë" + "Përsëritni asnjë" + "Përsëritni një" + diff --git a/extensions/mediasession/src/main/res/values-sr/strings.xml b/extensions/mediasession/src/main/res/values-sr/strings.xml new file mode 100644 index 0000000000..881cb2703b --- /dev/null +++ b/extensions/mediasession/src/main/res/values-sr/strings.xml @@ -0,0 +1,18 @@ + + + + diff --git a/extensions/mediasession/src/main/res/values-sv/strings.xml b/extensions/mediasession/src/main/res/values-sv/strings.xml new file mode 100644 index 0000000000..3a7bb630aa --- /dev/null +++ b/extensions/mediasession/src/main/res/values-sv/strings.xml @@ -0,0 +1,21 @@ + + + + "Upprepa alla" + "Upprepa inga" + "Upprepa en" + diff --git a/extensions/mediasession/src/main/res/values-sw/strings.xml b/extensions/mediasession/src/main/res/values-sw/strings.xml new file mode 100644 index 0000000000..726012ab88 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-sw/strings.xml @@ -0,0 +1,21 @@ + + + + "Rudia zote" + "Usirudie Yoyote" + "Rudia Moja" + diff --git a/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml new file mode 100644 index 0000000000..9364bc0be2 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ta-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "அனைத்தையும் மீண்டும் இயக்கு" + "எதையும் மீண்டும் இயக்காதே" + "ஒன்றை மட்டும் மீண்டும் இயக்கு" + diff --git a/extensions/mediasession/src/main/res/values-te-rIN/strings.xml b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml new file mode 100644 index 0000000000..b7ee7345d5 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-te-rIN/strings.xml @@ -0,0 +1,21 @@ + + + + "అన్నీ పునరావృతం చేయి" + "ఏదీ పునరావృతం చేయవద్దు" + "ఒకదాన్ని పునరావృతం చేయి" + diff --git a/extensions/mediasession/src/main/res/values-th/strings.xml b/extensions/mediasession/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..af502b3a4c --- /dev/null +++ b/extensions/mediasession/src/main/res/values-th/strings.xml @@ -0,0 +1,21 @@ + + + + "เล่นซ้ำทั้งหมด" + "ไม่เล่นซ้ำ" + "เล่นซ้ำรายการเดียว" + diff --git a/extensions/mediasession/src/main/res/values-tl/strings.xml b/extensions/mediasession/src/main/res/values-tl/strings.xml new file mode 100644 index 0000000000..239972a4c7 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-tl/strings.xml @@ -0,0 +1,21 @@ + + + + "Ulitin Lahat" + "Walang Uulitin" + "Ulitin ang Isa" + diff --git a/extensions/mediasession/src/main/res/values-tr/strings.xml b/extensions/mediasession/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000000..89a98b1ed9 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-tr/strings.xml @@ -0,0 +1,21 @@ + + + + "Tümünü Tekrarla" + "Hiçbirini Tekrarlama" + "Birini Tekrarla" + diff --git a/extensions/mediasession/src/main/res/values-uk/strings.xml b/extensions/mediasession/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..4e1d25eb8a --- /dev/null +++ b/extensions/mediasession/src/main/res/values-uk/strings.xml @@ -0,0 +1,21 @@ + + + + "Повторити все" + "Не повторювати" + "Повторити один елемент" + diff --git a/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml new file mode 100644 index 0000000000..ab2631a4ec --- /dev/null +++ b/extensions/mediasession/src/main/res/values-ur-rPK/strings.xml @@ -0,0 +1,21 @@ + + + + "سبھی کو دہرائیں" + "کسی کو نہ دہرائیں" + "ایک کو دہرائیں" + diff --git a/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml new file mode 100644 index 0000000000..c32d00af8e --- /dev/null +++ b/extensions/mediasession/src/main/res/values-uz-rUZ/strings.xml @@ -0,0 +1,21 @@ + + + + "Barchasini takrorlash" + "Takrorlamaslik" + "Bir marta takrorlash" + diff --git a/extensions/mediasession/src/main/res/values-vi/strings.xml b/extensions/mediasession/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..dabc9e05d5 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-vi/strings.xml @@ -0,0 +1,21 @@ + + + + "Lặp lại tất cả" + "Không lặp lại" + "Lặp lại một mục" + diff --git a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..beb3403cb9 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,21 @@ + + + + "重复播放全部" + "不重复播放" + "重复播放单个视频" + diff --git a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000000..775cd6441c --- /dev/null +++ b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,21 @@ + + + + "重複播放所有媒體項目" + "不重複播放任何媒體項目" + "重複播放一個媒體項目" + diff --git a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..d3789f4145 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,21 @@ + + + + "重複播放所有媒體項目" + "不重複播放" + "重複播放單一媒體項目" + diff --git a/extensions/mediasession/src/main/res/values-zu/strings.xml b/extensions/mediasession/src/main/res/values-zu/strings.xml new file mode 100644 index 0000000000..789b6fecb4 --- /dev/null +++ b/extensions/mediasession/src/main/res/values-zu/strings.xml @@ -0,0 +1,21 @@ + + + + "Phinda konke" + "Ungaphindi lutho" + "Phida okukodwa" + diff --git a/extensions/mediasession/src/main/res/values/strings.xml b/extensions/mediasession/src/main/res/values/strings.xml new file mode 100644 index 0000000000..72a67ff01c --- /dev/null +++ b/extensions/mediasession/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + + Repeat none + Repeat one + Repeat all + diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index d84dcb44ec..b10c4ba629 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -1,23 +1,16 @@ -# ExoPlayer OkHttp Extension # +# ExoPlayer OkHttp extension # ## Description ## -The OkHttp Extension is an [HttpDataSource][] implementation using Square's +The OkHttp extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. -## Using the extension ## +[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html +[OkHttp]: https://square.github.io/okhttp/ -The easiest way to use the extension is to add it as a gradle dependency. You -need to make sure you have the jcenter repository included in the `build.gradle` -file in the root of your project: +## Getting the extension ## -```gradle -repositories { - jcenter() -} -``` - -Next, include the following in your module's `build.gradle` file: +The easiest way to use the extension is to add it as a gradle dependency: ```gradle compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X' @@ -26,5 +19,33 @@ compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X' where `rX.X.X` is the version, which must match the version of the ExoPlayer library being used. -[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html -[OkHttp]: https://square.github.io/okhttp/ +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +If your application only needs to play http(s) content, using the OkHttp +extension is as simple as updating any `DataSource`s and `DataSource.Factory` +instantiations in your application code to use `OkHttpDataSource` and +`OkHttpDataSourceFactory` respectively. If your application also needs to play +non-http(s) content such as local files, use +``` +new DefaultDataSource( + ... + new OkHttpDataSource(...) /* baseDataSource argument */); +``` +and +``` +new DefaultDataSourceFactory( + ... + new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */); +``` +respectively. diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index f47f1a8556..bc9e0eba3e 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -29,8 +30,8 @@ android { } dependencies { - compile project(':library-core') - compile('com.squareup.okhttp3:okhttp:3.6.0') { + compile project(modulePrefix + 'library-core') + compile('com.squareup.okhttp3:okhttp:3.8.1') { exclude group: 'org.json' } } diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 167fc68e86..0519673e50 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -19,6 +19,7 @@ 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.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -45,6 +46,10 @@ import okhttp3.Response; */ public class OkHttpDataSource implements HttpDataSource { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp"); + } + private static final AtomicReference skipBufferReference = new AtomicReference<>(); @NonNull private final Call.Factory callFactory; diff --git a/extensions/opus/README.md b/extensions/opus/README.md index 36ca2b7261..e5f5bcb168 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -1,20 +1,19 @@ -# ExoPlayer Opus Extension # +# ExoPlayer Opus extension # ## Description ## -The Opus Extension is a [Renderer][] implementation that helps you bundle +The Opus extension is a [Renderer][] implementation that helps you bundle libopus (the Opus decoding library) into your app and use it along with ExoPlayer to play Opus audio on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -26,8 +25,6 @@ OPUS_EXT_PATH="${EXOPLAYER_ROOT}/extensions/opus/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -52,23 +49,8 @@ cd "${OPUS_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=all -j4 ``` -* In your project, you can add a dependency to the Opus Extension by using a -rule like this: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-opus' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-opus') -} -``` - -* Now, when you build your app, the Opus extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html ## Notes ## diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 31d5450fdd..41b428070f 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -30,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') } ext { diff --git a/extensions/opus/src/androidTest/AndroidManifest.xml b/extensions/opus/src/androidTest/AndroidManifest.xml index c819529692..e77590dc65 100644 --- a/extensions/opus/src/androidTest/AndroidManifest.xml +++ b/extensions/opus/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 263934d982..4c576b2cc0 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -57,7 +58,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener { + private static class TestPlaybackThread extends Thread implements Player.EventListener { private final Context context; private final Uri uri; @@ -120,12 +121,17 @@ public class OpusPlaybackTest extends InstrumentationTestCase { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playbackException != null)) { releasePlayerAndQuitLooper(); } } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 564a41fc77..730473ddad 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -32,6 +33,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { private static final int NUM_BUFFERS = 16; private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + private OpusDecoder decoder; + public LibopusAudioRenderer() { this(null, null); } @@ -51,6 +54,13 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, @@ -69,8 +79,16 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws OpusDecoderException { - return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, format.initializationData, mediaCrypto); + return decoder; + } + + @Override + protected Format getOutputFormat() { + return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, + Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT, + null, null, 0, null); } } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 95c38c34bb..b4a4622346 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -197,6 +197,20 @@ import java.util.List; opusClose(nativeDecoderContext); } + /** + * Returns the channel count of output audio. + */ + public int getChannelCount() { + return channelCount; + } + + /** + * Returns the sample rate of output audio. + */ + public int getSampleRate() { + return SAMPLE_RATE; + } + private static int nsToSamples(long ns) { return (int) (ns * SAMPLE_RATE / 1000000000); } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 41a28b9fd7..22985ea497 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.opus; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; /** @@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader; */ public final class OpusLibrary { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.opus"); + } + private static final LibraryLoader LOADER = new LibraryLoader("opus", "opusJNI"); private OpusLibrary() {} @@ -30,6 +35,8 @@ public final class OpusLibrary { * Override the names of the Opus native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibopusAudioRenderer} instance. + * + * @param libraries The names of the Opus native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md new file mode 100644 index 0000000000..042d7078dc --- /dev/null +++ b/extensions/rtmp/README.md @@ -0,0 +1,27 @@ +# ExoPlayer RTMP extension # + +## Description ## + +The RTMP extension is a [DataSource][] implementation for playing [RTMP][] +streams using [LibRtmp Client for Android][]. + +[DataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html +[RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol +[LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android + +## Using the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +compile 'com.google.android.exoplayer:extension-rtmp:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle new file mode 100644 index 0000000000..c832cb82e9 --- /dev/null +++ b/extensions/rtmp/build.gradle @@ -0,0 +1,41 @@ +// 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. +apply from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 15 + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + compile project(modulePrefix + 'library-core') + compile 'net.butterflytv.utils:rtmp-client:0.2.8' +} + +ext { + javadocTitle = 'RTMP extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-rtmp' + releaseDescription = 'RTMP extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/rtmp/src/main/AndroidManifest.xml b/extensions/rtmp/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..7c5e92c198 --- /dev/null +++ b/extensions/rtmp/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java new file mode 100644 index 0000000000..0601af4a2f --- /dev/null +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSource.java @@ -0,0 +1,97 @@ +/* + * 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.rtmp; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; +import java.io.IOException; +import net.butterflytv.rtmp_client.RtmpClient; +import net.butterflytv.rtmp_client.RtmpClient.RtmpIOException; + +/** + * A Real-Time Messaging Protocol (RTMP) {@link DataSource}. + */ +public final class RtmpDataSource implements DataSource { + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.rtmp"); + } + + @Nullable private final TransferListener listener; + + private RtmpClient rtmpClient; + private Uri uri; + + public RtmpDataSource() { + this(null); + } + + /** + * @param listener An optional listener. + */ + public RtmpDataSource(@Nullable TransferListener listener) { + this.listener = listener; + } + + @Override + public long open(DataSpec dataSpec) throws RtmpIOException { + rtmpClient = new RtmpClient(); + rtmpClient.open(dataSpec.uri.toString(), false); + + this.uri = dataSpec.uri; + if (listener != null) { + listener.onTransferStart(this, dataSpec); + } + return C.LENGTH_UNSET; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int bytesRead = rtmpClient.read(buffer, offset, readLength); + if (bytesRead == -1) { + return C.RESULT_END_OF_INPUT; + } + if (listener != null) { + listener.onBytesTransferred(this, bytesRead); + } + return bytesRead; + } + + @Override + public void close() { + if (uri != null) { + uri = null; + if (listener != null) { + listener.onTransferEnd(this); + } + } + if (rtmpClient != null) { + rtmpClient.close(); + rtmpClient = null; + } + } + + @Override + public Uri getUri() { + return uri; + } + +} diff --git a/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java new file mode 100644 index 0000000000..0510e9c7da --- /dev/null +++ b/extensions/rtmp/src/main/java/com/google/android/exoplayer2/ext/rtmp/RtmpDataSourceFactory.java @@ -0,0 +1,47 @@ +/* + * 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.rtmp; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import com.google.android.exoplayer2.upstream.TransferListener; + +/** + * A {@link Factory} that produces {@link RtmpDataSource}. + */ +public final class RtmpDataSourceFactory implements DataSource.Factory { + + @Nullable + private final TransferListener listener; + + public RtmpDataSourceFactory() { + this(null); + } + + /** + * @param listener An optional listener. + */ + public RtmpDataSourceFactory(@Nullable TransferListener listener) { + this.listener = listener; + } + + @Override + public DataSource createDataSource() { + return new RtmpDataSource(listener); + } + +} diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 53ef4b0bfd..87c5c8d54f 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -1,20 +1,19 @@ -# ExoPlayer VP9 Extension # +# ExoPlayer VP9 extension # ## Description ## -The VP9 Extension is a [Renderer][] implementation that helps you bundle libvpx +The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx (the VP9 decoding library) into your app and use it along with ExoPlayer to play VP9 video on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## -* Checkout ExoPlayer along with Extensions: - -``` -git clone https://github.com/google/ExoPlayer.git -``` +To use this extension you need to clone the ExoPlayer repository and depend on +its modules locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. In addition, it's necessary to build the extension's +native components as follows: * Set the following environment variables: @@ -26,8 +25,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" * Download the [Android NDK][] and set its location in an environment variable: -[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html - ``` NDK_PATH="" ``` @@ -66,23 +63,8 @@ cd "${VP9_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI=all -j4 ``` -* In your project, you can add a dependency to the VP9 Extension by using a the - following rule: - -``` -// in settings.gradle -include ':..:ExoPlayer:library' -include ':..:ExoPlayer:extension-vp9' - -// in build.gradle -dependencies { - compile project(':..:ExoPlayer:library') - compile project(':..:ExoPlayer:extension-vp9') -} -``` - -* Now, when you build your app, the VP9 extension will be built and the native - libraries will be packaged along with the APK. +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html ## Notes ## @@ -94,4 +76,3 @@ dependencies { `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But please note that `generate_libvpx_android_configs.sh` and the makefiles need to be modified to work with arbitrary versions of libvpx and libyuv. - diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 5068586a4a..de6dc65f74 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -30,7 +31,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') } ext { diff --git a/extensions/vp9/src/androidTest/AndroidManifest.xml b/extensions/vp9/src/androidTest/AndroidManifest.xml index d9fa8af2c3..b8b28fc346 100644 --- a/extensions/vp9/src/androidTest/AndroidManifest.xml +++ b/extensions/vp9/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 2647776b74..0bc945174e 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -86,7 +87,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener { + private static class TestPlaybackThread extends Thread implements Player.EventListener { private final Context context; private final Uri uri; @@ -152,12 +153,17 @@ public class VpxPlaybackTest extends InstrumentationTestCase { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playbackException != null)) { releasePlayerAndQuitLooper(); } } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 29423547b6..a947378de5 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -20,6 +20,7 @@ import android.graphics.Canvas; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.view.Surface; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; @@ -30,6 +31,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.Assertions; @@ -38,12 +40,35 @@ import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Decodes and renders video using the native VP9 decoder. */ public final class LibvpxVideoRenderer extends BaseRenderer { + @Retention(RetentionPolicy.SOURCE) + @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, + REINITIALIZATION_STATE_WAIT_END_OF_STREAM}) + private @interface ReinitializationState {} + /** + * The decoder does not need to be re-initialized. + */ + private static final int REINITIALIZATION_STATE_NONE = 0; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, but we + * haven't yet signaled an end of stream to the existing decoder. We need to do so in order to + * ensure that it outputs any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The input format has changed in a way that requires the decoder to be re-initialized, and we've + * signaled an end of stream to the existing decoder. We're waiting for the decoder to output an + * end of stream signal to indicate that it has output any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + /** * The type of a message that can be passed to an instance of this class via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object @@ -52,11 +77,18 @@ public final class LibvpxVideoRenderer extends BaseRenderer { public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE; /** - * The number of input buffers and the number of output buffers. The renderer may limit the - * minimum possible value due to requiring multiple output buffers to be dequeued at a time for it - * to make progress. + * The number of input buffers. + */ + private static final int NUM_INPUT_BUFFERS = 8; + /** + * The number of output buffers. The renderer may limit the minimum possible value due to + * requiring multiple output buffers to be dequeued at a time for it to make progress. + */ + private static final int NUM_OUTPUT_BUFFERS = 16; + /** + * The initial input buffer size. Input buffers are reallocated dynamically if this value is + * insufficient. */ - private static final int NUM_BUFFERS = 16; private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. private final boolean scaleToFit; @@ -71,12 +103,16 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private DecoderCounters decoderCounters; private Format format; private VpxDecoder decoder; - private DecoderInputBuffer inputBuffer; + private VpxInputBuffer inputBuffer; private VpxOutputBuffer outputBuffer; private VpxOutputBuffer nextOutputBuffer; private DrmSession drmSession; private DrmSession pendingDrmSession; + @ReinitializationState + private int decoderReinitializationState; + private boolean decoderReceivedBuffers; + private Bitmap bitmap; private boolean renderedFirstFrame; private long joiningDeadlineMs; @@ -153,6 +189,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = VpxDecoder.OUTPUT_MODE_NONE; + decoderReinitializationState = REINITIALIZATION_STATE_NONE; } @Override @@ -185,49 +222,25 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - // We have a format. - drmSession = pendingDrmSession; - ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto(); - } else { - // The drm session isn't open yet. - return; - } - } - try { - if (decoder == null) { - // If we don't have a decoder yet, we need to instantiate one. - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto); - decoder.setOutputMode(outputMode); + // If we don't have a decoder yet, we need to instantiate one. + maybeInitDecoder(); + + if (decoder != null) { + try { + // Rendering loop. + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs)) {} + while (feedInputBuffer()) {} TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - decoderCounters.decoderInitCount++; + } catch (VpxDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); } - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs)) {} - while (feedInputBuffer()) {} - TraceUtil.endSection(); - } catch (VpxDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + decoderCounters.ensureUpdated(); } - decoderCounters.ensureUpdated(); } - private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException { - if (outputStreamEnded) { - return false; - } - + private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException, + VpxDecoderException { // Acquire outputBuffer either from nextOutputBuffer or from the decoder. if (outputBuffer == null) { if (nextOutputBuffer != null) { @@ -247,15 +260,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } if (outputBuffer.isEndOfStream()) { - outputStreamEnded = true; - outputBuffer.release(); - outputBuffer = null; + if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the decoder, and have now processed all final buffers. + releaseDecoder(); + maybeInitDecoder(); + } else { + outputBuffer.release(); + outputBuffer = null; + outputStreamEnded = true; + } return false; } if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. - if (outputBuffer.timeUs <= positionUs) { + if (isBufferLate(outputBuffer.timeUs - positionUs)) { skipBuffer(); return true; } @@ -280,7 +299,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } - /** * Returns whether the current frame should be dropped. * @@ -293,10 +311,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { */ protected boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs, long positionUs, long joiningDeadlineMs) { - // Drop the frame if we're joining and are more than 30ms late, or if we have the next frame - // and that's also late. Else we'll render what we have. - return (joiningDeadlineMs != C.TIME_UNSET && outputBufferTimeUs < positionUs - 30000) - || (nextOutputBufferTimeUs != C.TIME_UNSET && nextOutputBufferTimeUs < positionUs); + return isBufferLate(outputBufferTimeUs - positionUs) + && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET); } private void renderBuffer() { @@ -356,7 +372,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { - if (inputStreamEnded) { + if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the decoder or the input stream has ended. return false; } @@ -367,6 +385,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + int result; if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. @@ -394,36 +420,43 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } inputBuffer.flip(); + inputBuffer.colorInfo = formatHolder.format.colorInfo; decoder.queueInputBuffer(inputBuffer); + decoderReceivedBuffers = true; decoderCounters.inputBufferCount++; inputBuffer = null; return true; } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } - int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = drmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } - private void flushDecoder() { - inputBuffer = null; + private void flushDecoder() throws ExoPlaybackException { waitingForKeys = false; - if (outputBuffer != null) { - outputBuffer.release(); - outputBuffer = null; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + if (nextOutputBuffer != null) { + nextOutputBuffer.release(); + nextOutputBuffer = null; + } + decoder.flush(); + decoderReceivedBuffers = false; } - if (nextOutputBuffer != null) { - nextOutputBuffer.release(); - nextOutputBuffer = null; - } - decoder.flush(); } @Override @@ -461,7 +494,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } @Override - protected void onPositionReset(long positionUs, boolean joining) { + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; clearRenderedFirstFrame(); @@ -490,8 +523,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { @Override protected void onDisabled() { - inputBuffer = null; - outputBuffer = null; format = null; waitingForKeys = false; clearReportedVideoSize(); @@ -518,20 +549,54 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private void releaseDecoder() { + private void maybeInitDecoder() throws ExoPlaybackException { if (decoder != null) { - decoder.release(); - decoder = null; - decoderCounters.decoderReleaseCount++; - waitingForKeys = false; - if (drmSession != null && pendingDrmSession != drmSession) { - try { - drmSessionManager.releaseSession(drmSession); - } finally { - drmSession = null; + return; + } + + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); } + // The drm session isn't open yet. + return; } } + + try { + long codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createVpxDecoder"); + decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + mediaCrypto); + decoder.setOutputMode(outputMode); + TraceUtil.endSection(); + long codecInitializedTimestamp = SystemClock.elapsedRealtime(); + eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, + codecInitializedTimestamp - codecInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (VpxDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + } + + private void releaseDecoder() { + if (decoder == null) { + return; + } + + inputBuffer = null; + outputBuffer = null; + nextOutputBuffer = null; + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + decoderReceivedBuffers = false; } private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { @@ -555,6 +620,17 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + if (pendingDrmSession != drmSession) { + if (decoderReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; + } else { + // There aren't any final output buffers, so release the decoder immediately. + releaseDecoder(); + maybeInitDecoder(); + } + } + eventDispatcher.inputFormatChanged(format); } @@ -654,4 +730,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; + } + } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 73ec7c2f96..4bec5bdf4c 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.CryptoInfo; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -27,7 +26,7 @@ import java.nio.ByteBuffer; * Vpx decoder. */ /* package */ final class VpxDecoder extends - SimpleDecoder { + SimpleDecoder { public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_YUV = 0; @@ -54,7 +53,7 @@ import java.nio.ByteBuffer; */ public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException { - super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); + super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); } @@ -85,8 +84,8 @@ import java.nio.ByteBuffer; } @Override - protected DecoderInputBuffer createInputBuffer() { - return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + protected VpxInputBuffer createInputBuffer() { + return new VpxInputBuffer(); } @Override @@ -100,7 +99,7 @@ import java.nio.ByteBuffer; } @Override - protected VpxDecoderException decode(DecoderInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, + protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); @@ -128,6 +127,7 @@ import java.nio.ByteBuffer; } else if (getFrameResult == -1) { return new VpxDecoderException("Buffer initialization failed."); } + outputBuffer.colorInfo = inputBuffer.colorInfo; return null; } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java new file mode 100644 index 0000000000..fcae9dc6bc --- /dev/null +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxInputBuffer.java @@ -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); + } + +} diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 24331127ec..854576b4b2 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; /** @@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader; */ public final class VpxLibrary { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.vpx"); + } + private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxJNI"); private VpxLibrary() {} @@ -30,6 +35,8 @@ public final class VpxLibrary { * Override the names of the Vpx native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibvpxVideoRenderer} instance. + * + * @param libraries The names of the Vpx native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); @@ -70,4 +77,5 @@ public final class VpxLibrary { private static native String vpxGetVersion(); private static native String vpxGetBuildConfig(); public static native boolean vpxIsSecureDecodeSupported(); + } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index db3cf49b0c..2618bf7c62 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.vp9; import com.google.android.exoplayer2.decoder.OutputBuffer; +import com.google.android.exoplayer2.video.ColorInfo; import java.nio.ByteBuffer; /** @@ -37,6 +38,8 @@ import java.nio.ByteBuffer; public ByteBuffer data; public int width; public int height; + public ColorInfo colorInfo; + /** * YUV planes for YUV mode. */ diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java index 8f43a0207b..d07e24d920 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java @@ -22,6 +22,8 @@ public interface VpxOutputBufferRenderer { /** * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. + * + * @param outputBuffer The output buffer to be rendered. */ void setOutputBuffer(VpxOutputBuffer outputBuffer); diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8c0a9b91f6..fc42154505 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Mar 13 11:17:14 GMT 2017 +#Wed Jul 12 10:31:13 BST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-all.zip diff --git a/library/all/build.gradle b/library/all/build.gradle index 63943ada77..79ed9c747b 100644 --- a/library/all/build.gradle +++ b/library/all/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -24,11 +25,11 @@ android { } dependencies { - compile project(':library-core') - compile project(':library-dash') - compile project(':library-hls') - compile project(':library-smoothstreaming') - compile project(':library-ui') + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') + compile project(modulePrefix + 'library-ui') } ext { diff --git a/library/core/build.gradle b/library/core/build.gradle index bb0adaa4c7..ecad1e58b5 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. apply plugin: 'com.android.library' +apply from: '../../constants.gradle' android { compileSdkVersion project.ext.compileSdkVersion @@ -22,6 +23,7 @@ android { targetSdkVersion project.ext.targetSdkVersion } + // Workaround to prevent circular dependency on project :testutils. sourceSets { androidTest { java.srcDirs += "../../testutils/src/main/java/" @@ -29,9 +31,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index a50de35b62..aeddc611cf 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -31,7 +31,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump index 1932ab78f7..f533e14c3f 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -30,5 +30,6 @@ track 1: time = 0 flags = 1073741824 data = length 39, hash B7FE77F4 + crypto mode = 1 encryption key = length 16, hash 4CE944CF tracksEnded = true diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index 8751c99b20..d84c549dea 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -30,5 +30,6 @@ track 1: time = 0 flags = 1073741824 data = length 24, hash E58668B1 + crypto mode = 1 encryption key = length 16, hash 4CE944CF tracksEnded = true diff --git a/library/core/src/androidTest/assets/ssa/empty b/library/core/src/androidTest/assets/ssa/empty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/library/core/src/androidTest/assets/ssa/invalid_timecodes b/library/core/src/androidTest/assets/ssa/invalid_timecodes new file mode 100644 index 0000000000..89f3bb3f1c --- /dev/null +++ b/library/core/src/androidTest/assets/ssa/invalid_timecodes @@ -0,0 +1,12 @@ +[Script Info] +Title: SomeTitle + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,Invalid,0:00:01.23,Default,Olly,This is the first subtitle{ignored}. +Dialogue: 0,0:00:02.34,Invalid,Default,Olly,This is the second subtitle \nwith a newline \Nand another. +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma. \ No newline at end of file diff --git a/library/core/src/androidTest/assets/ssa/no_end_timecodes b/library/core/src/androidTest/assets/ssa/no_end_timecodes new file mode 100644 index 0000000000..c2c57ac64e --- /dev/null +++ b/library/core/src/androidTest/assets/ssa/no_end_timecodes @@ -0,0 +1,12 @@ +[Script Info] +Title: SomeTitle + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00, ,Default,Olly,This is the first subtitle. +Dialogue: 0,0:00:02.34, ,Default,Olly,This is the second subtitle \nwith a newline \Nand another. +Dialogue: 0,0:00:04.56, ,Default,Olly,This is the third subtitle, with a comma. \ No newline at end of file diff --git a/library/core/src/androidTest/assets/ssa/typical b/library/core/src/androidTest/assets/ssa/typical new file mode 100644 index 0000000000..8a49099c5c --- /dev/null +++ b/library/core/src/androidTest/assets/ssa/typical @@ -0,0 +1,12 @@ +[Script Info] +Title: SomeTitle + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1 + +[Events] +Format: Layer, Start, End, Style, Name, Text +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}. +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another. +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma. \ No newline at end of file diff --git a/library/core/src/androidTest/assets/ssa/typical_dialogue b/library/core/src/androidTest/assets/ssa/typical_dialogue new file mode 100644 index 0000000000..5cdab5a84b --- /dev/null +++ b/library/core/src/androidTest/assets/ssa/typical_dialogue @@ -0,0 +1,3 @@ +Dialogue: 0,0:00:00.00,0:00:01.23,Default,Olly,This is the first subtitle{ignored}. +Dialogue: 0,0:00:02.34,0:00:03.45,Default,Olly,This is the second subtitle \nwith a newline \Nand another. +Dialogue: 0,0:00:04:56,0:00:08:90,Default,Olly,This is the third subtitle, with a comma. \ No newline at end of file diff --git a/library/core/src/androidTest/assets/ssa/typical_format b/library/core/src/androidTest/assets/ssa/typical_format new file mode 100644 index 0000000000..0cc5f1690f --- /dev/null +++ b/library/core/src/androidTest/assets/ssa/typical_format @@ -0,0 +1 @@ +Format: Layer, Start, End, Style, Name, Text \ No newline at end of file diff --git a/library/core/src/androidTest/assets/ssa/typical_header b/library/core/src/androidTest/assets/ssa/typical_header new file mode 100644 index 0000000000..3e96bcf14e --- /dev/null +++ b/library/core/src/androidTest/assets/ssa/typical_header @@ -0,0 +1,6 @@ +[Script Info] +Title: SomeTitle + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Open Sans Semibold,36,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,-1,0,0,0,100,100,0,0,1,1.7,0,2,0,0,28,1 \ No newline at end of file diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump index 3b44fb6fb9..98f3c6a85a 100644 --- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump @@ -69,7 +69,7 @@ track 224: sample 0: time = 40000 flags = 1 - data = length 20616, hash CA38A5B5 + data = length 20646, hash 576390B sample 1: time = 80000 flags = 0 diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index 26c6665aaa..91e48b1722 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -30,7 +30,7 @@ track 256: sample 0: time = 33366 flags = 1 - data = length 20669, hash 26DABA0F + data = length 20711, hash 34341E8 sample 1: time = 66733 flags = 0 diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2c10bfe6a0..bf4ea6e972 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,28 +15,20 @@ */ package com.google.android.exoplayer2; -import android.os.Handler; -import android.os.HandlerThread; import android.util.Pair; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MediaClock; +import com.google.android.exoplayer2.testutil.ExoPlayerWrapper; +import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.util.MimeTypes; -import java.io.IOException; -import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import junit.framework.TestCase; /** @@ -62,10 +54,10 @@ public final class ExoPlayerTest extends TestCase { * error. */ public void testPlayEmptyTimeline() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = Timeline.EMPTY; MediaSource mediaSource = new FakeMediaSource(timeline, null); - FakeRenderer renderer = new FakeRenderer(null); + FakeRenderer renderer = new FakeRenderer(); playerWrapper.setup(mediaSource, renderer); playerWrapper.blockUntilEnded(TIMEOUT_MS); assertEquals(0, playerWrapper.positionDiscontinuityCount); @@ -79,7 +71,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes a single period. */ public void testPlaySinglePeriodTimeline() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); Object manifest = new Object(); MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT); @@ -98,7 +90,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes three periods. */ public void testPlayMultiPeriodTimeline() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0), @@ -119,7 +111,7 @@ public final class ExoPlayerTest extends TestCase { * source. */ public void testReadAheadToEndDoesNotResetRenderer() throws Exception { - final PlayerWrapper playerWrapper = new PlayerWrapper(); + final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(false, false, 10), new TimelineWindowDefinition(false, false, 10), @@ -166,7 +158,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepreparationGivesFreshSourceInfo() throws Exception { - PlayerWrapper playerWrapper = new PlayerWrapper(); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); @@ -218,501 +210,54 @@ public final class ExoPlayerTest extends TestCase { Pair.create(timeline, thirdSourceManifest)); } - /** - * Wraps a player with its own handler thread. - */ - private static final class PlayerWrapper implements ExoPlayer.EventListener { - - private final CountDownLatch sourceInfoCountDownLatch; - private final CountDownLatch endedCountDownLatch; - private final HandlerThread playerThread; - private final Handler handler; - private final LinkedList> sourceInfos; - - private ExoPlayer player; - private TrackGroupArray trackGroups; - private Exception exception; - - // Written only on the main thread. - private volatile int positionDiscontinuityCount; - - public PlayerWrapper() { - sourceInfoCountDownLatch = new CountDownLatch(1); - endedCountDownLatch = new CountDownLatch(1); - playerThread = new HandlerThread("ExoPlayerTest thread"); - playerThread.start(); - handler = new Handler(playerThread.getLooper()); - sourceInfos = new LinkedList<>(); - } - - // Called on the test thread. - - public void blockUntilEnded(long timeoutMs) throws Exception { - if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - exception = new TimeoutException("Test playback timed out waiting for playback to end."); - } - release(); - // Throw any pending exception (from playback, timing out or releasing). - if (exception != null) { - throw exception; - } - } - - public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception { - if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - throw new TimeoutException("Test playback timed out waiting for source info."); - } - } - - public void setup(final MediaSource mediaSource, final Renderer... renderers) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); - player.addListener(PlayerWrapper.this); - player.setPlayWhenReady(true); - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } + public void testRepeatModeChanges() throws Exception { + Timeline timeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 100000), + new TimelineWindowDefinition(true, false, 100000), + new TimelineWindowDefinition(true, false, 100000)); + final int[] actionSchedule = { // 0 -> 1 + Player.REPEAT_MODE_ONE, // 1 -> 1 + Player.REPEAT_MODE_OFF, // 1 -> 2 + Player.REPEAT_MODE_ONE, // 2 -> 2 + Player.REPEAT_MODE_ALL, // 2 -> 0 + Player.REPEAT_MODE_ONE, // 0 -> 0 + -1, // 0 -> 0 + Player.REPEAT_MODE_OFF, // 0 -> 1 + -1, // 1 -> 2 + -1 // 2 -> ended + }; + int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2}; + final LinkedList windowIndices = new LinkedList<>(); + final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length); + ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() { + @Override + @SuppressWarnings("ResourceType") + public void onPositionDiscontinuity() { + super.onPositionDiscontinuity(); + int actionIndex = actionSchedule.length - (int) actionCounter.getCount(); + if (actionSchedule[actionIndex] != -1) { + player.setRepeatMode(actionSchedule[actionIndex]); } - }); - } - - public void prepare(final MediaSource mediaSource) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - public void release() throws InterruptedException { - handler.post(new Runnable() { - @Override - public void run() { - try { - if (player != null) { - player.release(); - } - } catch (Exception e) { - handleError(e); - } finally { - playerThread.quit(); - } - } - }); - playerThread.join(); - } - - private void handleError(Exception exception) { - if (this.exception == null) { - this.exception = exception; + windowIndices.add(player.getCurrentWindowIndex()); + actionCounter.countDown(); } - endedCountDownLatch.countDown(); + }; + MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); + playerWrapper.setup(mediaSource, renderer); + boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); + playerWrapper.release(); + assertTrue("Test playback timed out waiting for action schedule to end.", finished); + if (playerWrapper.exception != null) { + throw playerWrapper.exception; } - - @SafeVarargs - public final void assertSourceInfosEquals(Pair... sourceInfos) { - assertEquals(sourceInfos.length, this.sourceInfos.size()); - for (Pair sourceInfo : sourceInfos) { - assertEquals(sourceInfo, this.sourceInfos.remove()); - } + assertEquals(expectedWindowIndices.length, windowIndices.size()); + for (int i = 0; i < expectedWindowIndices.length; i++) { + assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue()); } - - // ExoPlayer.EventListener implementation. - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED) { - endedCountDownLatch.countDown(); - } - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - sourceInfos.add(Pair.create(timeline, manifest)); - sourceInfoCountDownLatch.countDown(); - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - this.trackGroups = trackGroups; - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - handleError(exception); - } - - @SuppressWarnings("NonAtomicVolatileUpdate") - @Override - public void onPositionDiscontinuity() { - positionDiscontinuityCount++; - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - } - - private static final class TimelineWindowDefinition { - - public final boolean isSeekable; - public final boolean isDynamic; - public final long durationUs; - - public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { - this.isSeekable = isSeekable; - this.isDynamic = isDynamic; - this.durationUs = durationUs; - } - - } - - private static final class FakeTimeline extends Timeline { - - private final TimelineWindowDefinition[] windowDefinitions; - - public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { - this.windowDefinitions = windowDefinitions; - } - - @Override - public int getWindowCount() { - return windowDefinitions.length; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; - Object id = setIds ? windowIndex : null; - return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable, - windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0); - } - - @Override - public int getPeriodCount() { - return windowDefinitions.length; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; - Object id = setIds ? periodIndex : null; - return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false); - } - - @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Integer)) { - return C.INDEX_UNSET; - } - int index = (Integer) uid; - return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET; - } - - } - - /** - * Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating - * the period will return a {@link FakeMediaPeriod}. - */ - private static class FakeMediaSource implements MediaSource { - - private final Timeline timeline; - private final Object manifest; - private final TrackGroupArray trackGroupArray; - private final ArrayList activeMediaPeriods; - - private boolean preparedSource; - private boolean releasedSource; - - public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { - this.timeline = timeline; - this.manifest = manifest; - TrackGroup[] trackGroups = new TrackGroup[formats.length]; - for (int i = 0; i < formats.length; i++) { - trackGroups[i] = new TrackGroup(formats[i]); - } - trackGroupArray = new TrackGroupArray(trackGroups); - activeMediaPeriods = new ArrayList<>(); - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - assertFalse(preparedSource); - preparedSource = true; - listener.onSourceInfoRefreshed(timeline, manifest); - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - assertTrue(preparedSource); - } - - @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkIndex(index, 0, timeline.getPeriodCount()); - assertTrue(preparedSource); - assertFalse(releasedSource); - assertEquals(0, positionUs); - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); - activeMediaPeriods.add(mediaPeriod); - return mediaPeriod; - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - assertTrue(preparedSource); - assertFalse(releasedSource); - FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod; - assertTrue(activeMediaPeriods.remove(fakeMediaPeriod)); - fakeMediaPeriod.release(); - } - - @Override - public void releaseSource() { - assertTrue(preparedSource); - assertFalse(releasedSource); - assertTrue(activeMediaPeriods.isEmpty()); - releasedSource = true; - } - - } - - /** - * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that - * track will give the player a {@link FakeSampleStream}. - */ - private static final class FakeMediaPeriod implements MediaPeriod { - - private final TrackGroupArray trackGroupArray; - - private boolean preparedPeriod; - - public FakeMediaPeriod(TrackGroupArray trackGroupArray) { - this.trackGroupArray = trackGroupArray; - } - - public void release() { - preparedPeriod = false; - } - - @Override - public void prepare(Callback callback) { - assertFalse(preparedPeriod); - preparedPeriod = true; - callback.onPrepared(this); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - assertTrue(preparedPeriod); - } - - @Override - public TrackGroupArray getTrackGroups() { - assertTrue(preparedPeriod); - return trackGroupArray; - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - assertTrue(preparedPeriod); - int rendererCount = selections.length; - for (int i = 0; i < rendererCount; i++) { - if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - streams[i] = null; - } - } - for (int i = 0; i < rendererCount; i++) { - if (streams[i] == null && selections[i] != null) { - TrackSelection selection = selections[i]; - assertEquals(1, selection.length()); - assertEquals(0, selection.getIndexInTrackGroup(0)); - TrackGroup trackGroup = selection.getTrackGroup(); - assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET); - streams[i] = new FakeSampleStream(trackGroup.getFormat(0)); - streamResetFlags[i] = true; - } - } - return 0; - } - - @Override - public void discardBuffer(long positionUs) { - // Do nothing. - } - - @Override - public long readDiscontinuity() { - assertTrue(preparedPeriod); - return C.TIME_UNSET; - } - - @Override - public long getBufferedPositionUs() { - assertTrue(preparedPeriod); - return C.TIME_END_OF_SOURCE; - } - - @Override - public long seekToUs(long positionUs) { - assertTrue(preparedPeriod); - assertEquals(0, positionUs); - return positionUs; - } - - @Override - public long getNextLoadPositionUs() { - assertTrue(preparedPeriod); - return C.TIME_END_OF_SOURCE; - } - - @Override - public boolean continueLoading(long positionUs) { - assertTrue(preparedPeriod); - return false; - } - - } - - /** - * Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag - * on its input buffer. - */ - private static final class FakeSampleStream implements SampleStream { - - private final Format format; - - private boolean readFormat; - - public FakeSampleStream(Format format) { - this.format = format; - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { - if (formatRequired || !readFormat) { - formatHolder.format = format; - readFormat = true; - return C.RESULT_FORMAT_READ; - } else { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } - } - - @Override - public void maybeThrowError() throws IOException { - // Do nothing. - } - - @Override - public void skipData(long positionUs) { - // Do nothing. - } - - } - - /** - * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer - * verifies that it reads a given {@link Format}. - */ - private static class FakeRenderer extends BaseRenderer { - - private final Format expectedFormat; - - public int positionResetCount; - public int formatReadCount; - public int bufferReadCount; - public boolean isEnded; - - public FakeRenderer(Format expectedFormat) { - super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN - : MimeTypes.getTrackType(expectedFormat.sampleMimeType)); - this.expectedFormat = expectedFormat; - } - - @Override - protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - positionResetCount++; - isEnded = false; - } - - @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (isEnded) { - return; - } - - // Verify the format matches the expected format. - FormatHolder formatHolder = new FormatHolder(); - DecoderInputBuffer buffer = - new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); - int result = readSource(formatHolder, buffer, false); - if (result == C.RESULT_FORMAT_READ) { - formatReadCount++; - assertEquals(expectedFormat, formatHolder.format); - } else if (result == C.RESULT_BUFFER_READ) { - bufferReadCount++; - if (buffer.isEndOfStream()) { - isEnded = true; - } - } - } - - @Override - public boolean isReady() { - return isSourceReady(); - } - - @Override - public boolean isEnded() { - return isEnded; - } - - @Override - public int supportsFormat(Format format) throws ExoPlaybackException { - return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED - : FORMAT_UNSUPPORTED_TYPE; - } - - } - - private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock { - - public FakeMediaClockRenderer(Format expectedFormat) { - super(expectedFormat); - } - - @Override - public MediaClock getMediaClock() { - return this; - } - + assertEquals(9, playerWrapper.positionDiscontinuityCount); + assertTrue(renderer.isEnded); + playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java index a47a3fb12d..bdea08638b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java @@ -53,9 +53,9 @@ public final class FormatTest extends TestCase { } public void testParcelable() { - DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4, + DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM, + DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM, TestUtil.buildTestData(128, 1 /* data seed */)); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); byte[] projectionData = new byte[] {1, 2, 3}; @@ -94,10 +94,10 @@ public final class FormatTest extends TestCase { 500, 128, 5, 44100, INIT_DATA, null, 0, null)); testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, 500, Format.NO_VALUE, 5, 44100, null, null, 0, null)); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", null, - Format.NO_VALUE, 0, "eng", null)); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", null, - Format.NO_VALUE, 0, null, null)); + testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, + "eng")); + testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, + null)); } @SuppressLint("InlinedApi") diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java new file mode 100644 index 0000000000..d9ee27bd62 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + } +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index df2e8756a5..aa8cbfdb62 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -31,16 +31,16 @@ import junit.framework.TestCase; */ public class DrmInitDataTest extends TestCase { - private static final SchemeData DATA_1 = - new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - private static final SchemeData DATA_2 = - new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); - private static final SchemeData DATA_1B = - new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - private static final SchemeData DATA_2B = - new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); - private static final SchemeData DATA_UNIVERSAL = - new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */)); + private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4, + TestUtil.buildTestData(128, 1 /* data seed */)); + private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4, + TestUtil.buildTestData(128, 2 /* data seed */)); + private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4, + TestUtil.buildTestData(128, 1 /* data seed */)); + private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4, + TestUtil.buildTestData(128, 2 /* data seed */)); + private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4, + TestUtil.buildTestData(128, 3 /* data seed */)); public void testParcelable() { DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index afd690762b..9f5b067b5e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -154,7 +154,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType", new byte[] {1, 4, 7, 0, 3, 6})); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java index 321181621e..fc8d181eac 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.flv; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link FlvExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class FlvExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new FlvExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java index 48eee69b50..624a5ccb7e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mkv; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Tests for {@link MatroskaExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class MatroskaExtractorTest extends InstrumentationTestCase { public void testMkvSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -34,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryption() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -43,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java index c70710f1ee..0f98624d69 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.mp3; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link Mp3Extractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class Mp3ExtractorTest extends InstrumentationTestCase { public void testMp3Sample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); @@ -34,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase { } public void testTrimmedMp3Sample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index 95ad8b446e..76c13495c1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link FragmentedMp4Extractor}. @@ -26,26 +27,28 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); + ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4", + getInstrumentation()); } public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), + ExtractorAsserts.assertBehavior( + getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), "mp4/sample_fragmented_sei.mp4", getInstrumentation()); } public void testAtomWithZeroSize() throws Exception { - TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", + ExtractorAsserts.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", getInstrumentation(), ParserException.class); } - private static TestUtil.ExtractorFactory getExtractorFactory() { + private static ExtractorFactory getExtractorFactory() { return getExtractorFactory(0); } - private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) { - return new TestUtil.ExtractorFactory() { + private static ExtractorFactory getExtractorFactory(final int flags) { + return new ExtractorFactory() { @Override public Extractor create() { return new FragmentedMp4Extractor(flags, null); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index 6ad777da70..5e327e5502 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.mp4; import android.annotation.TargetApi; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Tests for {@link Mp4Extractor}. @@ -27,7 +28,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class Mp4ExtractorTest extends InstrumentationTestCase { public void testMp4Sample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp4Extractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java index 04a6131652..3be23422cc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java @@ -17,9 +17,10 @@ package com.google.android.exoplayer2.extractor.ogg; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.testutil.TestUtil.ExtractorFactory; import java.io.IOException; /** @@ -35,20 +36,22 @@ public final class OggExtractorTest extends InstrumentationTestCase { }; public void testOpus() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); } public void testFlac() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", + getInstrumentation()); } public void testFlacNoSeektable() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", getInstrumentation()); } public void testVorbis() throws Exception { - TestUtil.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", + getInstrumentation()); } public void testSniffVorbis() throws Exception { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index 4e99e2745e..18050f48a3 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -19,7 +19,8 @@ import android.annotation.TargetApi; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.util.MimeTypes; /** @@ -29,8 +30,8 @@ import com.google.android.exoplayer2.util.MimeTypes; public final class RawCcExtractorTest extends InstrumentationTestCase { public void testRawCcSample() throws Exception { - TestUtil.assertOutput( - new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { @Override public Extractor create() { return new RawCcExtractor( diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java index ab44e3aed3..31633361db 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link Ac3Extractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class Ac3ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Ac3Extractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index e30a863d07..9eb65d2091 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link AdtsExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class AdtsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new AdtsExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java index ef97bef0ff..78ef05a769 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link PsExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class PsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new PsExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index efd653b8d9..b6eddb5112 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -25,6 +25,8 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; @@ -43,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new TsExtractor(); @@ -65,7 +67,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); fileData = out.toByteArray(); - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertOutput(new ExtractorFactory() { @Override public Extractor create() { return new TsExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index a416d644b7..36c05aa72e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -17,7 +17,8 @@ package com.google.android.exoplayer2.extractor.wav; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; /** * Unit test for {@link WavExtractor}. @@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class WavExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - TestUtil.assertOutput(new TestUtil.ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new WavExtractor(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 0933fb858b..66b0337450 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -15,20 +15,17 @@ */ package com.google.android.exoplayer2.source; -import static org.mockito.Mockito.doAnswer; - import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TestUtil; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import com.google.android.exoplayer2.testutil.TimelineAsserts; /** * Unit tests for {@link ClippingMediaSource}. @@ -38,15 +35,11 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { private static final long TEST_PERIOD_DURATION_US = 1000000; private static final long TEST_CLIP_AMOUNT_US = 300000; - @Mock - private MediaSource mockMediaSource; - private Timeline clippedTimeline; private Window window; private Period period; @Override protected void setUp() throws Exception { - TestUtil.setUpMockito(this); window = new Timeline.Window(); period = new Timeline.Period(); } @@ -109,35 +102,29 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } + public void testWindowAndPeriodIndices() { + Timeline timeline = new FakeTimeline( + new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US)); + Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, + TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + TimelineAsserts.assertWindowIds(clippedTimeline, 111); + TimelineAsserts.assertPeriodCounts(clippedTimeline, 1); + TimelineAsserts.assertPreviousWindowIndices( + clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); + } + /** * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ - private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - mockMediaSourceSourceWithTimeline(timeline); - new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true, - new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - clippedTimeline = timeline; - } - }); - return clippedTimeline; - } - - /** - * Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info. - */ - private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) { - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2]; - listener.onSourceInfoRefreshed(timeline, null); - return null; - } - }).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(), - Mockito.any(MediaSource.Listener.class)); - return mockMediaSource; + private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { + MediaSource mediaSource = new FakeMediaSource(timeline, null); + return TestUtil.extractTimelineFromMediaSource( + new ClippingMediaSource(mediaSource, startMs, endMs)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java new file mode 100644 index 0000000000..3bf89f9bcc --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -0,0 +1,115 @@ +/* + * 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.Player; +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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + + timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); + TimelineAsserts.assertWindowIds(timeline, 111); + TimelineAsserts.assertPeriodCounts(timeline, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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)); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java new file mode 100644 index 0000000000..f8636b9990 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -0,0 +1,558 @@ +/* + * 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.Player; +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); + + // Move sources. + mediaSource.moveMediaSource(2, 3); + waitForTimelineUpdate(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); + mediaSource.moveMediaSource(3, 2); + waitForTimelineUpdate(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); + mediaSource.moveMediaSource(0, 6); + waitForTimelineUpdate(); + TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); + TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); + mediaSource.moveMediaSource(6, 0); + 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, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.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.moveMediaSource(0, 2); + mediaSource.removeMediaSource(0); + mediaSource.moveMediaSource(1, 0); + 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(Player.EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeListener(Player.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 long getContentPosition() { + 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; + } + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java new file mode 100644 index 0000000000..d2045c29a5 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -0,0 +1,94 @@ +/* + * 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.Player; +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, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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, Player.REPEAT_MODE_OFF, + C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, + 8, 0, 1, 2, 3, 4, 5, 6, 7); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, + 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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, Player.REPEAT_MODE_OFF, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.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)); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java new file mode 100644 index 0000000000..76ea0e34cf --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -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|xx|x|x|||xx| + */ + 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(); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java new file mode 100644 index 0000000000..9ed4d79307 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -0,0 +1,123 @@ +/* + * 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.text.ssa; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Unit test for {@link SsaDecoder}. + */ +public final class SsaDecoderTest extends InstrumentationTestCase { + + private static final String EMPTY = "ssa/empty"; + private static final String TYPICAL = "ssa/typical"; + private static final String TYPICAL_HEADER_ONLY = "ssa/typical_header"; + private static final String TYPICAL_DIALOGUE_ONLY = "ssa/typical_dialogue"; + private static final String TYPICAL_FORMAT_ONLY = "ssa/typical_format"; + private static final String INVALID_TIMECODES = "ssa/invalid_timecodes"; + private static final String NO_END_TIMECODES = "ssa/no_end_timecodes"; + + public void testDecodeEmpty() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY); + SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(0, subtitle.getEventTimeCount()); + assertTrue(subtitle.getCues(0).isEmpty()); + } + + public void testDecodeTypical() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL); + SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(6, subtitle.getEventTimeCount()); + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + assertTypicalCue3(subtitle, 4); + } + + public void testDecodeTypicalWithInitializationData() throws IOException { + byte[] headerBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_HEADER_ONLY); + byte[] formatBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FORMAT_ONLY); + ArrayList initializationData = new ArrayList<>(); + initializationData.add(formatBytes); + initializationData.add(headerBytes); + SsaDecoder decoder = new SsaDecoder(initializationData); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_DIALOGUE_ONLY); + SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(6, subtitle.getEventTimeCount()); + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + assertTypicalCue3(subtitle, 4); + } + + public void testDecodeInvalidTimecodes() throws IOException { + // Parsing should succeed, parsing the third cue only. + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), INVALID_TIMECODES); + SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(2, subtitle.getEventTimeCount()); + assertTypicalCue3(subtitle, 0); + } + + public void testDecodeNoEndTimecodes() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES); + SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(3, subtitle.getEventTimeCount()); + + assertEquals(0, subtitle.getEventTime(0)); + assertEquals("This is the first subtitle.", + subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); + + assertEquals(2340000, subtitle.getEventTime(1)); + assertEquals("This is the second subtitle \nwith a newline \nand another.", + subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString()); + + assertEquals(4560000, subtitle.getEventTime(2)); + assertEquals("This is the third subtitle, with a comma.", + subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); + } + + private static void assertTypicalCue1(SsaSubtitle subtitle, int eventIndex) { + assertEquals(0, subtitle.getEventTime(eventIndex)); + assertEquals("This is the first subtitle.", + subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()); + assertEquals(1230000, subtitle.getEventTime(eventIndex + 1)); + } + + private static void assertTypicalCue2(SsaSubtitle subtitle, int eventIndex) { + assertEquals(2340000, subtitle.getEventTime(eventIndex)); + assertEquals("This is the second subtitle \nwith a newline \nand another.", + subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()); + assertEquals(3450000, subtitle.getEventTime(eventIndex + 1)); + } + + private static void assertTypicalCue3(SsaSubtitle subtitle, int eventIndex) { + assertEquals(4560000, subtitle.getEventTime(eventIndex)); + assertEquals("This is the third subtitle, with a comma.", + subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()); + assertEquals(8900000, subtitle.getEventTime(eventIndex + 1)); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 880a214fb3..167499fcdc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -37,7 +37,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); - // Assert that the subtitle is empty. + assertEquals(0, subtitle.getEventTimeCount()); assertTrue(subtitle.getCues(0).isEmpty()); } @@ -46,6 +46,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertEquals(6, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -56,6 +57,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertEquals(6, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -66,6 +68,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertEquals(6, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue2(subtitle, 2); @@ -77,6 +80,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertEquals(4, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue3(subtitle, 2); @@ -87,6 +91,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertEquals(4, subtitle.getEventTimeCount()); assertTypicalCue1(subtitle, 0); assertTypicalCue3(subtitle, 2); @@ -97,6 +102,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + assertEquals(2, subtitle.getEventTimeCount()); assertTypicalCue3(subtitle, 0); } @@ -106,20 +112,16 @@ public final class SubripDecoderTest extends InstrumentationTestCase { byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); - // Test event count. assertEquals(3, subtitle.getEventTimeCount()); - // Test first cue. assertEquals(0, subtitle.getEventTime(0)); assertEquals("SubRip doesn't technically allow missing end timecodes.", subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()); - // Test second cue. assertEquals(2345000, subtitle.getEventTime(1)); assertEquals("We interpret it to mean that a subtitle extends to the start of the next one.", subtitle.getCues(subtitle.getEventTime(1)).get(0).text.toString()); - // Test third cue. assertEquals(3456000, subtitle.getEventTime(2)); assertEquals("Or to the end of the media.", subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString()); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java index f471370e4c..d6be100877 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java @@ -125,25 +125,25 @@ public final class CssParserTest extends InstrumentationTestCase { String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n"; ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput)); StringBuilder builder = new StringBuilder(); - assertEquals(CssParser.parseNextToken(input, builder), "lorem"); - assertEquals(CssParser.parseNextToken(input, builder), ":"); - assertEquals(CssParser.parseNextToken(input, builder), "ipsum"); - assertEquals(CssParser.parseNextToken(input, builder), "{"); - assertEquals(CssParser.parseNextToken(input, builder), "dolor"); - assertEquals(CssParser.parseNextToken(input, builder), "}"); - assertEquals(CssParser.parseNextToken(input, builder), "#sit"); - assertEquals(CssParser.parseNextToken(input, builder), ","); - assertEquals(CssParser.parseNextToken(input, builder), "amet"); - assertEquals(CssParser.parseNextToken(input, builder), ";"); - assertEquals(CssParser.parseNextToken(input, builder), "lorem"); - assertEquals(CssParser.parseNextToken(input, builder), ":"); - assertEquals(CssParser.parseNextToken(input, builder), "ipsum"); - assertEquals(CssParser.parseNextToken(input, builder), "dolor"); - assertEquals(CssParser.parseNextToken(input, builder), "("); - assertEquals(CssParser.parseNextToken(input, builder), "("); - assertEquals(CssParser.parseNextToken(input, builder), ")"); - assertEquals(CssParser.parseNextToken(input, builder), ")"); - assertEquals(CssParser.parseNextToken(input, builder), null); + assertEquals("lorem", CssParser.parseNextToken(input, builder)); + assertEquals(":", CssParser.parseNextToken(input, builder)); + assertEquals("ipsum", CssParser.parseNextToken(input, builder)); + assertEquals("{", CssParser.parseNextToken(input, builder)); + assertEquals("dolor", CssParser.parseNextToken(input, builder)); + assertEquals("}", CssParser.parseNextToken(input, builder)); + assertEquals("#sit", CssParser.parseNextToken(input, builder)); + assertEquals(",", CssParser.parseNextToken(input, builder)); + assertEquals("amet", CssParser.parseNextToken(input, builder)); + assertEquals(";", CssParser.parseNextToken(input, builder)); + assertEquals("lorem", CssParser.parseNextToken(input, builder)); + assertEquals(":", CssParser.parseNextToken(input, builder)); + assertEquals("ipsum", CssParser.parseNextToken(input, builder)); + assertEquals("dolor", CssParser.parseNextToken(input, builder)); + assertEquals("(", CssParser.parseNextToken(input, builder)); + assertEquals("(", CssParser.parseNextToken(input, builder)); + assertEquals(")", CssParser.parseNextToken(input, builder)); + assertEquals(")", CssParser.parseNextToken(input, builder)); + assertEquals(null, CssParser.parseNextToken(input, builder)); } public void testStyleScoreSystem() { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index c76e4989d8..e7ff2a6811 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -15,12 +15,14 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; + import android.net.Uri; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.FakeDataSource.FakeData; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.util.Util; @@ -38,27 +40,29 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private static final String KEY_1 = "key 1"; private static final String KEY_2 = "key 2"; - private File cacheDir; - private SimpleCache simpleCache; + private File tempFolder; + private SimpleCache cache; @Override - protected void setUp() throws Exception { - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); - simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); + public void setUp() throws Exception { + super.setUp(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } @Override - protected void tearDown() throws Exception { - Util.recursiveDelete(cacheDir); + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + super.tearDown(); } public void testMaxCacheFileSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, false, false); - File[] files = cacheDir.listFiles(); - for (File file : files) { - if (!file.getName().equals(CachedContentIndex.FILE_NAME)) { - assertTrue(file.length() <= MAX_CACHE_FILE_SIZE); + for (String key : cache.getKeys()) { + for (CacheSpan cacheSpan : cache.getCachedSpans(key)) { + assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE); + assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE); } } } @@ -104,7 +108,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { // Read partial at EOS but don't cross it so length is unknown CacheDataSource cacheDataSource = createCacheDataSource(false, true); assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2); - assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); + assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1)); // Now do an unbounded request for whole data. This will cause a bounded request from upstream. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. @@ -124,13 +128,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase { CacheDataSource cacheDataSource = createCacheDataSource(false, true, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS); assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET); - MoreAsserts.assertEmpty(simpleCache.getKeys()); + MoreAsserts.assertEmpty(cache.getKeys()); } public void testReadOnlyCache() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); assertReadDataContentLength(cacheDataSource, false, false); - assertEquals(0, cacheDir.list().length); + assertCacheEmpty(cache); } private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) @@ -155,30 +159,30 @@ public class CacheDataSourceTest extends InstrumentationTestCase { assertReadData(cacheDataSource, unknownLength, 0, length); assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache " + "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length, - simpleCache.getContentLength(KEY_1)); + cache.getContentLength(KEY_1)); } private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position, int length) throws IOException { - int actualLength = TEST_DATA.length - position; + int testDataLength = TEST_DATA.length - position; if (length != C.LENGTH_UNSET) { - actualLength = Math.min(actualLength, length); + testDataLength = Math.min(testDataLength, length); } - assertEquals(unknownLength ? length : actualLength, + assertEquals(unknownLength ? length : testDataLength, cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1))); byte[] buffer = new byte[100]; - int index = 0; + int totalBytesRead = 0; while (true) { - int read = cacheDataSource.read(buffer, index, buffer.length - index); + int read = cacheDataSource.read(buffer, totalBytesRead, buffer.length - totalBytesRead); if (read == C.RESULT_END_OF_INPUT) { break; } - index += read; + totalBytesRead += read; } - assertEquals(actualLength, index); - MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + actualLength), - Arrays.copyOf(buffer, index)); + assertEquals(testDataLength, totalBytesRead); + MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength), + Arrays.copyOf(buffer, totalBytesRead)); cacheDataSource.close(); } @@ -192,7 +196,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { return createCacheDataSource(setReadException, simulateUnknownLength, flags, - new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE)); + new CacheDataSink(cache, MAX_CACHE_FILE_SIZE)); } private CacheDataSource createCacheDataSource(boolean setReadException, @@ -204,7 +208,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { if (setReadException) { fakeData.appendReadError(new IOException("Shouldn't read from upstream")); } - return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, + return new CacheDataSource(cache, upstream, new FileDataSource(), cacheWriteDataSink, flags, null); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java new file mode 100644 index 0000000000..df9975d43b --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -0,0 +1,291 @@ +/* + * 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.cache; + +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; + +import android.net.Uri; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; +import java.io.File; +import org.mockito.Answers; +import org.mockito.Mock; + +/** + * Tests {@link CacheUtil}. + */ +public class CacheUtilTest extends InstrumentationTestCase { + + /** + * Abstract fake Cache implementation used by the test. This class must be public so Mockito can + * create a proxy for it. + */ + public abstract static class AbstractFakeCache implements Cache { + + // This array is set to alternating length of cached and not cached regions in tests: + // spansAndGaps = {, , + // , , ... } + // Ideally it should end with a cached region but it shouldn't matter for any code. + private int[] spansAndGaps; + private long contentLength; + + private void init() { + spansAndGaps = new int[] {}; + contentLength = C.LENGTH_UNSET; + } + + @Override + public long getCachedBytes(String key, long position, long length) { + for (int i = 0; i < spansAndGaps.length; i++) { + int spanOrGap = spansAndGaps[i]; + if (position < spanOrGap) { + long left = Math.min(spanOrGap - position, length); + return (i & 1) == 1 ? -left : left; + } + position -= spanOrGap; + } + return -length; + } + + @Override + public long getContentLength(String key) { + return contentLength; + } + } + + @Mock(answer = Answers.CALLS_REAL_METHODS) private AbstractFakeCache mockCache; + private File tempFolder; + private SimpleCache cache; + + @Override + public void setUp() throws Exception { + super.setUp(); + TestUtil.setUpMockito(this); + mockCache.init(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + } + + @Override + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + super.tearDown(); + } + + public void testGenerateKey() throws Exception { + assertNotNull(CacheUtil.generateKey(Uri.EMPTY)); + + Uri testUri = Uri.parse("test"); + String key = CacheUtil.generateKey(testUri); + assertNotNull(key); + + // Should generate the same key for the same input + assertEquals(key, CacheUtil.generateKey(testUri)); + + // Should generate different key for different input + assertFalse(key.equals(CacheUtil.generateKey(Uri.parse("test2")))); + } + + public void testGetKey() throws Exception { + Uri testUri = Uri.parse("test"); + String key = "key"; + // If DataSpec.key is present, returns it + assertEquals(key, CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, key))); + // If not generates a new one using DataSpec.uri + assertEquals(CacheUtil.generateKey(testUri), + CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, null))); + } + + public void testGetCachedNoData() throws Exception { + CachingCounters counters = new CachingCounters(); + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters); + + assertCounters(counters, 0, 0, C.LENGTH_UNSET); + } + + public void testGetCachedDataUnknownLength() throws Exception { + // Mock there is 100 bytes cached at the beginning + mockCache.spansAndGaps = new int[] {100}; + CachingCounters counters = new CachingCounters(); + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters); + + assertCounters(counters, 100, 0, C.LENGTH_UNSET); + } + + public void testGetCachedNoDataKnownLength() throws Exception { + mockCache.contentLength = 1000; + CachingCounters counters = new CachingCounters(); + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters); + + assertCounters(counters, 0, 0, 1000); + } + + public void testGetCached() throws Exception { + mockCache.contentLength = 1000; + mockCache.spansAndGaps = new int[] {100, 100, 200}; + CachingCounters counters = new CachingCounters(); + CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters); + + assertCounters(counters, 300, 0, 1000); + } + + public void testCache() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + CachingCounters counters = new CachingCounters(); + CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters); + + assertCounters(counters, 0, 100, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheSetOffsetAndLength() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); + CachingCounters counters = new CachingCounters(); + CacheUtil.cache(dataSpec, cache, dataSource, counters); + + assertCounters(counters, 0, 20, 20); + + CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters); + + assertCounters(counters, 20, 80, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheUnknownLength() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") + .setSimulateUnknownLength(true) + .appendReadData(TestUtil.buildTestData(100)).endData(); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + DataSpec dataSpec = new DataSpec(Uri.parse("test_data")); + CachingCounters counters = new CachingCounters(); + CacheUtil.cache(dataSpec, cache, dataSource, counters); + + assertCounters(counters, 0, 100, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheUnknownLengthPartialCaching() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") + .setSimulateUnknownLength(true) + .appendReadData(TestUtil.buildTestData(100)).endData(); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 10, 20, null); + CachingCounters counters = new CachingCounters(); + CacheUtil.cache(dataSpec, cache, dataSource, counters); + + assertCounters(counters, 0, 20, 20); + + CacheUtil.cache(new DataSpec(testUri), cache, dataSource, counters); + + assertCounters(counters, 20, 80, 100); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheLengthExceedsActualDataLength() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); + CachingCounters counters = new CachingCounters(); + CacheUtil.cache(dataSpec, cache, dataSource, counters); + + assertCounters(counters, 0, 100, 1000); + assertCachedData(cache, fakeDataSet); + } + + public void testCacheThrowEOFException() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri testUri = Uri.parse("test_data"); + DataSpec dataSpec = new DataSpec(testUri, 0, 1000, null); + + try { + CacheUtil.cache(dataSpec, cache, new CacheDataSource(cache, dataSource), + new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, + /*enableEOFException*/ true); + fail(); + } catch (EOFException e) { + // Do nothing. + } + } + + public void testCachePolling() throws Exception { + final CachingCounters counters = new CachingCounters(); + FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") + .appendReadData(TestUtil.buildTestData(100)) + .appendReadAction(new Runnable() { + @Override + public void run() { + assertCounters(counters, 0, 100, 300); + } + }) + .appendReadData(TestUtil.buildTestData(100)) + .appendReadAction(new Runnable() { + @Override + public void run() { + assertCounters(counters, 0, 200, 300); + } + }) + .appendReadData(TestUtil.buildTestData(100)).endData(); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + CacheUtil.cache(new DataSpec(Uri.parse("test_data")), cache, dataSource, counters); + + assertCounters(counters, 0, 300, 300); + assertCachedData(cache, fakeDataSet); + } + + public void testRemove() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + + Uri uri = Uri.parse("test_data"); + CacheUtil.cache(new DataSpec(uri), cache, + // set maxCacheFileSize to 10 to make sure there are multiple spans + new CacheDataSource(cache, dataSource, 0, 10), + new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], null, 0, null, true); + CacheUtil.remove(cache, CacheUtil.generateKey(uri)); + + assertCacheEmpty(cache); + } + + private static void assertCounters(CachingCounters counters, int alreadyCachedBytes, + int newlyCachedBytes, int contentLength) { + assertEquals(alreadyCachedBytes, counters.alreadyCachedBytes); + assertEquals(newlyCachedBytes, counters.newlyCachedBytes); + assertEquals(contentLength, counters.contentLength); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 396584a39e..a88a1dd615 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -96,7 +96,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { this.stream = stream; readEndOfStream = false; streamOffsetUs = offsetUs; - onStreamChanged(formats); + onStreamChanged(formats, offsetUs); } @Override @@ -183,16 +183,19 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * The default implementation is a no-op. * * @param formats The enabled formats. + * @param offsetUs The offset that will be added to the timestamps of buffers read via + * {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} so that decoder input + * buffers have monotonically increasing timestamps. * @throws ExoPlaybackException If an error occurs. */ - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { // Do nothing. } /** * Called when the position is reset. This occurs when the renderer is enabled after - * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity - * is encountered. + * {@link #onStreamChanged(Format[], long)} has been called, and also when a position + * discontinuity is encountered. *

* After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples * starting from a key frame. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 35a69df39e..d7d0ed40aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -31,6 +31,7 @@ import java.util.UUID; /** * Defines constants used by the library. */ +@SuppressWarnings("InlinedApi") public final class C { private C() {} @@ -83,12 +84,12 @@ public final class C { public static final String UTF16_NAME = "UTF-16"; /** - * * The name of the serif font family. + * The name of the serif font family. */ public static final String SERIF_NAME = "serif"; /** - * * The name of the sans-serif font family. + * The name of the sans-serif font family. */ public static final String SANS_SERIF_NAME = "sans-serif"; @@ -101,24 +102,20 @@ public final class C { /** * @see MediaCodec#CRYPTO_MODE_UNENCRYPTED */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED; /** * @see MediaCodec#CRYPTO_MODE_AES_CTR */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR; /** * @see MediaCodec#CRYPTO_MODE_AES_CBC */ - @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; /** * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. */ - @SuppressWarnings("InlinedApi") public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; /** @@ -160,28 +157,24 @@ public final class C { /** * @see AudioFormat#ENCODING_AC3 */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** * @see AudioFormat#ENCODING_E_AC3 */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; /** * @see AudioFormat#ENCODING_DTS */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; /** * @see AudioFormat#ENCODING_DTS_HD */ - @SuppressWarnings("InlinedApi") public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; /** * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND */ - @SuppressWarnings({"InlinedApi", "deprecation"}) + @SuppressWarnings("deprecation") public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23 ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; @@ -189,13 +182,17 @@ public final class C { * Stream types for an {@link android.media.AudioTrack}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING, - STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL}) + @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_DTMF, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, + STREAM_TYPE_RING, STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL, STREAM_TYPE_USE_DEFAULT}) public @interface StreamType {} /** * @see AudioManager#STREAM_ALARM */ public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM; + /** + * @see AudioManager#STREAM_DTMF + */ + public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF; /** * @see AudioManager#STREAM_MUSIC */ @@ -216,11 +213,143 @@ public final class C { * @see AudioManager#STREAM_VOICE_CALL */ public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL; + /** + * @see AudioManager#USE_DEFAULT_STREAM_TYPE + */ + public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE; /** * The default stream type used by audio renderers. */ public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC; + /** + * Content types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({CONTENT_TYPE_MOVIE, CONTENT_TYPE_MUSIC, CONTENT_TYPE_SONIFICATION, CONTENT_TYPE_SPEECH, + CONTENT_TYPE_UNKNOWN}) + public @interface AudioContentType {} + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE + */ + public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC + */ + public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION + */ + public static final int CONTENT_TYPE_SONIFICATION = + android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH + */ + public static final int CONTENT_TYPE_SPEECH = + android.media.AudioAttributes.CONTENT_TYPE_SPEECH; + /** + * @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN + */ + public static final int CONTENT_TYPE_UNKNOWN = + android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN; + + /** + * Flags for {@link com.google.android.exoplayer2.audio.AudioAttributes}. + *

+ * Note that {@code FLAG_HW_AV_SYNC} is not available because the player takes care of setting the + * flag when tunneling is enabled via a track selector. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_AUDIBILITY_ENFORCED}) + public @interface AudioFlags {} + /** + * @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED + */ + public static final int FLAG_AUDIBILITY_ENFORCED = + android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; + + /** + * Usage types for {@link com.google.android.exoplayer2.audio.AudioAttributes}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({USAGE_ALARM, USAGE_ASSISTANCE_ACCESSIBILITY, USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, USAGE_GAME, USAGE_MEDIA, USAGE_NOTIFICATION, + USAGE_NOTIFICATION_COMMUNICATION_DELAYED, USAGE_NOTIFICATION_COMMUNICATION_INSTANT, + USAGE_NOTIFICATION_COMMUNICATION_REQUEST, USAGE_NOTIFICATION_EVENT, + USAGE_NOTIFICATION_RINGTONE, USAGE_UNKNOWN, USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING}) + public @interface AudioUsage {} + /** + * @see android.media.AudioAttributes#USAGE_ALARM + */ + public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY + */ + public static final int USAGE_ASSISTANCE_ACCESSIBILITY = + android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE + */ + public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = + android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; + /** + * @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION + */ + public static final int USAGE_ASSISTANCE_SONIFICATION = + android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + /** + * @see android.media.AudioAttributes#USAGE_GAME + */ + public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME; + /** + * @see android.media.AudioAttributes#USAGE_MEDIA + */ + public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION + */ + public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = + android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT + */ + public static final int USAGE_NOTIFICATION_EVENT = + android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT; + /** + * @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE + */ + public static final int USAGE_NOTIFICATION_RINGTONE = + android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + /** + * @see android.media.AudioAttributes#USAGE_UNKNOWN + */ + public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN; + /** + * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION + */ + public static final int USAGE_VOICE_COMMUNICATION = + android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; + /** + * @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING + */ + public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = + android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; + /** * Flags which can apply to a buffer containing a media sample. */ @@ -231,12 +360,10 @@ public final class C { /** * Indicates that a buffer holds a synchronization sample. */ - @SuppressWarnings("InlinedApi") public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME; /** * Flag for empty buffers that signal that the end of the stream was reached. */ - @SuppressWarnings("InlinedApi") public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; /** * Indicates that a buffer is (at least partially) encrypted. @@ -256,13 +383,11 @@ public final class C { /** * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT */ - @SuppressWarnings("InlinedApi") public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT; /** * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT */ - @SuppressWarnings("InlinedApi") public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING; /** @@ -453,6 +578,26 @@ public final class C { public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE; + /** + * "cenc" scheme type name as defined in ISO/IEC 23001-7:2016. + */ + public static final String CENC_TYPE_cenc = "cenc"; + + /** + * "cbc1" scheme type name as defined in ISO/IEC 23001-7:2016. + */ + public static final String CENC_TYPE_cbc1 = "cbc1"; + + /** + * "cens" scheme type name as defined in ISO/IEC 23001-7:2016. + */ + public static final String CENC_TYPE_cens = "cens"; + + /** + * "cbcs" scheme type name as defined in ISO/IEC 23001-7:2016. + */ + public static final String CENC_TYPE_cbcs = "cbcs"; + /** * The Nil UUID as defined by * RFC4122. @@ -498,16 +643,25 @@ public final class C { /** * A type of a message that can be passed to an audio {@link Renderer} via * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object - * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream - * type of the underlying {@link android.media.AudioTrack}. See also - * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type - * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}. + * should be an {@link com.google.android.exoplayer2.audio.AudioAttributes} instance that will + * configure the underlying audio track. If not set, the default audio attributes will be used. + * They are suitable for general media playback. *

- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can - * introduce a brief gap in audio output. Note also that tracks in the same audio session must - * share the same routing, so a new audio session id will be generated. + * Setting the audio attributes during playback may introduce a short gap in audio output as the + * audio track is recreated. A new audio session id will also be generated. + *

+ * If tunneling is enabled by the track selector, the specified audio attributes will be ignored, + * but they will take effect if audio is later played without tunneling. + *

+ * If the device is running a build before platform API version 21, audio attributes cannot be set + * directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + *

+ * To get audio attributes that are equivalent to a legacy stream type, pass the stream type to + * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build + * an audio attributes instance. */ - public static final int MSG_SET_STREAM_TYPE = 3; + public static final int MSG_SET_AUDIO_ATTRIBUTES = 3; /** * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer} @@ -564,17 +718,14 @@ public final class C { /** * @see MediaFormat#COLOR_STANDARD_BT709 */ - @SuppressWarnings("InlinedApi") public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709; /** * @see MediaFormat#COLOR_STANDARD_BT601_PAL */ - @SuppressWarnings("InlinedApi") public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL; /** * @see MediaFormat#COLOR_STANDARD_BT2020 */ - @SuppressWarnings("InlinedApi") public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; /** @@ -586,17 +737,14 @@ public final class C { /** * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO */ - @SuppressWarnings("InlinedApi") public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO; /** * @see MediaFormat#COLOR_TRANSFER_ST2084 */ - @SuppressWarnings("InlinedApi") public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084; /** * @see MediaFormat#COLOR_TRANSFER_HLG */ - @SuppressWarnings("InlinedApi") public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; /** @@ -608,12 +756,10 @@ public final class C { /** * @see MediaFormat#COLOR_RANGE_LIMITED */ - @SuppressWarnings("InlinedApi") public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED; /** * @see MediaFormat#COLOR_RANGE_FULL */ - @SuppressWarnings("InlinedApi") public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; /** @@ -632,24 +778,24 @@ public final class C { /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values. * * @param timeUs The time in microseconds. * @return The corresponding time in milliseconds. */ public static long usToMs(long timeUs) { - return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000); + return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000); } /** * Converts a time in milliseconds to the corresponding time in microseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values. * * @param timeMs The time in milliseconds. * @return The corresponding time in microseconds. */ public static long msToUs(long timeMs) { - return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); + return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 18bf9eeb8c..b096b5ae12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -16,29 +16,27 @@ package com.google.android.exoplayer2; import android.os.Looper; -import android.support.annotation.IntDef; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player exposing traditional high-level media player functionality, such as - * the ability to buffer media, play, pause and seek. Instances can be obtained from + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from * {@link ExoPlayerFactory}. * - *

Player composition

+ *

Player components

*

ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this @@ -46,18 +44,20 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * Components common to all ExoPlayer implementations are: *

*/ -public interface ExoPlayer { +public interface ExoPlayer extends Player { /** - * Listener of changes in player state. + * @deprecated Use {@link Player.EventListener} instead. */ - interface EventListener { - - /** - * Called when the timeline and/or manifest has been refreshed. - *

- * Note that if the timeline has changed then a position discontinuity may also have occurred. - * For example, the current period index may have changed as a result of periods being added or - * removed from the timeline. This will not be reported via a separate call to - * {@link #onPositionDiscontinuity()}. - * - * @param timeline The latest timeline. Never null, but may be empty. - * @param manifest The latest manifest. May be null. - */ - void onTimelineChanged(Timeline timeline, Object manifest); - - /** - * Called when the available or selected tracks change. - * - * @param trackGroups The available tracks. Never null, but may be of length zero. - * @param trackSelections The track selections for each {@link Renderer}. Never null and always - * of length {@link #getRendererCount()}, but may contain null elements. - */ - void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections); - - /** - * Called when the player starts or stops loading the source. - * - * @param isLoading Whether the source is currently being loaded. - */ - void onLoadingChanged(boolean isLoading); - - /** - * Called when the value returned from either {@link #getPlayWhenReady()} or - * {@link #getPlaybackState()} changes. - * - * @param playWhenReady Whether playback will proceed when ready. - * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer} - * interface. - */ - void onPlayerStateChanged(boolean playWhenReady, int playbackState); - - /** - * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} - * immediately after this method is called. The player instance can still be used, and - * {@link #release()} must still be called on the player should it no longer be required. - * - * @param error The error. - */ - void onPlayerError(ExoPlaybackException error); - - /** - * Called when a position discontinuity occurs without a change to the timeline. A position - * discontinuity occurs when the current window or period index changes (as a result of playback - * transitioning from one period in the timeline to the next), or when the playback position - * jumps within the period currently being played (as a result of a seek being performed, or - * when the source introduces a discontinuity internally). - *

- * When a position discontinuity occurs as a result of a change to the timeline this method is - * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. - */ - void onPositionDiscontinuity(); - - /** - * Called when the current playback parameters change. The playback parameters may change due to - * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself - * may change them (for example, if audio playback switches to passthrough mode, where speed - * adjustment is no longer possible). - * - * @param playbackParameters The playback parameters. - */ - void onPlaybackParametersChanged(PlaybackParameters playbackParameters); - - } + @Deprecated + interface EventListener extends Player.EventListener {} /** * A component of an {@link ExoPlayer} that can receive messages on the playback thread. @@ -236,47 +165,48 @@ public interface ExoPlayer { } /** - * The player does not have a source to play, so it is neither buffering nor ready to play. + * @deprecated Use {@link Player#STATE_IDLE} instead. */ - int STATE_IDLE = 1; + @Deprecated + int STATE_IDLE = Player.STATE_IDLE; /** - * The player not able to immediately play from the current position. The cause is - * {@link Renderer} specific, but this state typically occurs when more data needs to be - * loaded to be ready to play, or more data needs to be buffered for playback to resume. + * @deprecated Use {@link Player#STATE_BUFFERING} instead. */ - int STATE_BUFFERING = 2; + @Deprecated + int STATE_BUFFERING = Player.STATE_BUFFERING; /** - * The player is able to immediately play from the current position. The player will be playing if - * {@link #getPlayWhenReady()} returns true, and paused otherwise. + * @deprecated Use {@link Player#STATE_READY} instead. */ - int STATE_READY = 3; + @Deprecated + int STATE_READY = Player.STATE_READY; /** - * The player has finished playing the media. + * @deprecated Use {@link Player#STATE_ENDED} instead. */ - int STATE_ENDED = 4; + @Deprecated + int STATE_ENDED = Player.STATE_ENDED; /** - * Register a listener to receive events from the player. The listener's methods will be called on - * the thread that was used to construct the player. However, if the thread used to construct the - * player does not have a {@link Looper}, then the listener will be called on the main thread. - * - * @param listener The listener to register. + * @deprecated Use {@link Player#REPEAT_MODE_OFF} instead. */ - void addListener(EventListener listener); + @Deprecated + @RepeatMode int REPEAT_MODE_OFF = Player.REPEAT_MODE_OFF; + /** + * @deprecated Use {@link Player#REPEAT_MODE_ONE} instead. + */ + @Deprecated + @RepeatMode int REPEAT_MODE_ONE = Player.REPEAT_MODE_ONE; + /** + * @deprecated Use {@link Player#REPEAT_MODE_ALL} instead. + */ + @Deprecated + @RepeatMode int REPEAT_MODE_ALL = Player.REPEAT_MODE_ALL; /** - * Unregister a listener. The listener will no longer receive events from the player. + * Gets the {@link Looper} associated with the playback thread. * - * @param listener The listener to unregister. + * @return The {@link Looper} associated with the playback thread. */ - void removeListener(EventListener listener); - - /** - * Returns the current state of the player. - * - * @return One of the {@code STATE} constants defined in this interface. - */ - int getPlaybackState(); + Looper getPlaybackLooper(); /** * Prepares the player to play the provided {@link MediaSource}. Equivalent to @@ -298,104 +228,6 @@ public interface ExoPlayer { */ void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); - /** - * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. - *

- * If the player is already in the ready state then this method can be used to pause and resume - * playback. - * - * @param playWhenReady Whether playback should proceed when ready. - */ - void setPlayWhenReady(boolean playWhenReady); - - /** - * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. - * - * @return Whether playback will proceed when ready. - */ - boolean getPlayWhenReady(); - - /** - * Whether the player is currently loading the source. - * - * @return Whether the player is currently loading the source. - */ - boolean isLoading(); - - /** - * Seeks to the default position associated with the current window. The position can depend on - * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically - * be the live edge of the window. For other streams it will typically be the start of the window. - */ - void seekToDefaultPosition(); - - /** - * Seeks to the default position associated with the specified window. The position can depend on - * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically - * be the live edge of the window. For other streams it will typically be the start of the window. - * - * @param windowIndex The index of the window whose associated default position should be seeked - * to. - */ - void seekToDefaultPosition(int windowIndex); - - /** - * Seeks to a position specified in milliseconds in the current window. - * - * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to - * the window's default position. - */ - void seekTo(long positionMs); - - /** - * Seeks to a position specified in milliseconds in the specified window. - * - * @param windowIndex The index of the window. - * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to - * the window's default position. - */ - void seekTo(int windowIndex, long positionMs); - - /** - * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the - * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. - *

- * Playback parameters changes may cause the player to buffer. - * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever - * the currently active playback parameters change. When that listener is called, the parameters - * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch - * may be out of range, in which case they are constrained to a set of permitted values. If it is - * not possible to change the playback parameters, the listener will not be invoked. - * - * @param playbackParameters The playback parameters, or {@code null} to use the defaults. - */ - void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); - - /** - * Returns the currently active playback parameters. - * - * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) - */ - PlaybackParameters getPlaybackParameters(); - - /** - * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention - * is to pause playback. - *

- * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The - * player instance can still be used, and {@link #release()} must still be called on the player if - * it's no longer required. - *

- * Calling this method does not reset the playback position. - */ - void stop(); - - /** - * Releases the player. This method must be called when the player is no longer required. The - * player must not be used after calling this method. - */ - void release(); - /** * Sends messages to their target components. The messages are delivered on the playback thread. * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player @@ -413,88 +245,4 @@ public interface ExoPlayer { */ void blockingSendMessages(ExoPlayerMessage... messages); - /** - * Returns the number of renderers. - */ - int getRendererCount(); - - /** - * Returns the track type that the renderer at a given index handles. - * - * @see Renderer#getTrackType() - * @param index The index of the renderer. - * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. - */ - int getRendererType(int index); - - /** - * Returns the available track groups. - */ - TrackGroupArray getCurrentTrackGroups(); - - /** - * Returns the current track selections for each renderer. - */ - TrackSelectionArray getCurrentTrackSelections(); - - /** - * Returns the current manifest. The type depends on the {@link MediaSource} passed to - * {@link #prepare}. May be null. - */ - Object getCurrentManifest(); - - /** - * Returns the current {@link Timeline}. Never null, but may be empty. - */ - Timeline getCurrentTimeline(); - - /** - * Returns the index of the period currently being played. - */ - int getCurrentPeriodIndex(); - - /** - * Returns the index of the window currently being played. - */ - int getCurrentWindowIndex(); - - /** - * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the - * duration is not known. - */ - long getDuration(); - - /** - * Returns the playback position in the current window, in milliseconds. - */ - long getCurrentPosition(); - - /** - * Returns an estimate of the position in the current window up to which data is buffered, in - * milliseconds. - */ - long getBufferedPosition(); - - /** - * Returns an estimate of the percentage in the current window up to which data is buffered, or 0 - * if no estimate is available. - */ - int getBufferedPercentage(); - - /** - * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is - * empty. - * - * @see Timeline.Window#isDynamic - */ - boolean isCurrentWindowDynamic(); - - /** - * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is - * empty. - * - * @see Timeline.Window#isSeekable - */ - boolean isCurrentWindowSeekable(); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index c70d729fc3..c3a76cd962 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -24,6 +24,7 @@ import android.util.Log; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -45,12 +46,13 @@ import java.util.concurrent.CopyOnWriteArraySet; private final TrackSelectionArray emptyTrackSelections; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; - private final CopyOnWriteArraySet listeners; + private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; private boolean tracksSelected; private boolean playWhenReady; + private @RepeatMode int repeatMode; private int playbackState; private int pendingSeekAcks; private int pendingPrepareAcks; @@ -78,12 +80,14 @@ import java.util.concurrent.CopyOnWriteArraySet; */ @SuppressLint("HandlerLeak") public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION_SLASHY + " [" + Util.DEVICE_DEBUG_INFO + "]"); + Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); this.playWhenReady = false; - this.playbackState = STATE_IDLE; + this.repeatMode = Player.REPEAT_MODE_OFF; + this.playbackState = Player.STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); timeline = Timeline.EMPTY; @@ -101,16 +105,21 @@ import java.util.concurrent.CopyOnWriteArraySet; }; playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - eventHandler, playbackInfo, this); + repeatMode, eventHandler, playbackInfo, this); } @Override - public void addListener(EventListener listener) { + public Looper getPlaybackLooper() { + return internalPlayer.getPlaybackLooper(); + } + + @Override + public void addListener(Player.EventListener listener) { listeners.add(listener); } @Override - public void removeListener(EventListener listener) { + public void removeListener(Player.EventListener listener) { listeners.remove(listener); } @@ -130,7 +139,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (!timeline.isEmpty() || manifest != null) { timeline = Timeline.EMPTY; manifest = null; - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest); } } @@ -139,7 +148,7 @@ import java.util.concurrent.CopyOnWriteArraySet; trackGroups = TrackGroupArray.EMPTY; trackSelections = emptyTrackSelections; trackSelector.onSelectionActivated(null); - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onTracksChanged(trackGroups, trackSelections); } } @@ -153,7 +162,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (this.playWhenReady != playWhenReady) { this.playWhenReady = playWhenReady; internalPlayer.setPlayWhenReady(playWhenReady); - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onPlayerStateChanged(playWhenReady, playbackState); } } @@ -164,6 +173,22 @@ import java.util.concurrent.CopyOnWriteArraySet; return playWhenReady; } + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + if (this.repeatMode != repeatMode) { + this.repeatMode = repeatMode; + internalPlayer.setRepeatMode(repeatMode); + for (Player.EventListener listener : listeners) { + listener.onRepeatModeChanged(repeatMode); + } + } + } + + @Override + public @RepeatMode int getRepeatMode() { + return repeatMode; + } + @Override public boolean isLoading() { return isLoading; @@ -195,10 +220,10 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingPeriodIndex = 0; } else { timeline.getWindow(windowIndex, window); - long resolvedPositionMs = - positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs; + long resolvedPositionUs = + positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : C.msToUs(positionMs); int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs); + long periodPositionUs = window.getPositionInFirstPeriodUs() + resolvedPositionUs; long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs && periodIndex < window.lastPeriodIndex) { @@ -213,7 +238,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } else { maskingWindowPositionMs = positionMs; internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onPositionDiscontinuity(); } } @@ -239,6 +264,9 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void release() { + Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " [" + + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] [" + + ExoPlayerLibraryInfo.registeredModules() + "]"); internalPlayer.release(); eventHandler.removeCallbacksAndMessages(null); } @@ -258,7 +286,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingPeriodIndex; } else { - return playbackInfo.periodIndex; + return playbackInfo.periodId.periodIndex; } } @@ -267,7 +295,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowIndex; } else { - return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex; + return timeline.getPeriod(playbackInfo.periodId.periodIndex, period).windowIndex; } } @@ -276,7 +304,14 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty()) { return C.TIME_UNSET; } - return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + if (isPlayingAd()) { + MediaPeriodId periodId = playbackInfo.periodId; + timeline.getPeriod(periodId.periodIndex, period); + long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup); + return C.usToMs(adDurationUs); + } else { + return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); + } } @Override @@ -284,7 +319,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodIndex, period); + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs); } } @@ -295,7 +330,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodIndex, period); + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs); } } @@ -321,6 +356,31 @@ import java.util.concurrent.CopyOnWriteArraySet; return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable; } + @Override + public boolean isPlayingAd() { + return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; + } + + @Override + public int getCurrentAdGroupIndex() { + return pendingSeekAcks == 0 ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; + } + + @Override + public long getContentPosition() { + if (isPlayingAd()) { + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); + return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); + } else { + return getCurrentPosition(); + } + } + @Override public int getRendererCount() { return renderers.length; @@ -360,14 +420,14 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_STATE_CHANGED: { playbackState = msg.arg1; - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onPlayerStateChanged(playWhenReady, playbackState); } break; } case ExoPlayerImplInternal.MSG_LOADING_CHANGED: { isLoading = msg.arg1 != 0; - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onLoadingChanged(isLoading); } break; @@ -379,7 +439,7 @@ import java.util.concurrent.CopyOnWriteArraySet; trackGroups = trackSelectorResult.groups; trackSelections = trackSelectorResult.selections; trackSelector.onSelectionActivated(trackSelectorResult.info); - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onTracksChanged(trackGroups, trackSelections); } } @@ -389,7 +449,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; if (msg.arg1 != 0) { - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onPositionDiscontinuity(); } } @@ -399,7 +459,7 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { if (pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onPositionDiscontinuity(); } } @@ -412,7 +472,7 @@ import java.util.concurrent.CopyOnWriteArraySet; timeline = sourceInfo.timeline; manifest = sourceInfo.manifest; playbackInfo = sourceInfo.playbackInfo; - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest); } } @@ -422,7 +482,7 @@ import java.util.concurrent.CopyOnWriteArraySet; PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; if (!this.playbackParameters.equals(playbackParameters)) { this.playbackParameters = playbackParameters; - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onPlaybackParametersChanged(playbackParameters); } } @@ -430,7 +490,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_ERROR: { ExoPlaybackException exception = (ExoPlaybackException) msg.obj; - for (EventListener listener : listeners) { + for (Player.EventListener listener : listeners) { listener.onPlayerError(exception); } break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index bf5b3f6482..cb04501fc0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -17,14 +17,18 @@ package com.google.android.exoplayer2; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; +import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -48,21 +52,32 @@ import java.io.IOException; */ public static final class PlaybackInfo { - public final int periodIndex; + public final MediaPeriodId periodId; public final long startPositionUs; + public final long contentPositionUs; public volatile long positionUs; public volatile long bufferedPositionUs; public PlaybackInfo(int periodIndex, long startPositionUs) { - this.periodIndex = periodIndex; + this(new MediaPeriodId(periodIndex), startPositionUs); + } + + public PlaybackInfo(MediaPeriodId periodId, long startPositionUs) { + this(periodId, startPositionUs, C.TIME_UNSET); + } + + public PlaybackInfo(MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { + this.periodId = periodId; this.startPositionUs = startPositionUs; + this.contentPositionUs = contentPositionUs; positionUs = startPositionUs; bufferedPositionUs = startPositionUs; } public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs); + PlaybackInfo playbackInfo = new PlaybackInfo(periodId.copyWithPeriodIndex(periodIndex), + startPositionUs, contentPositionUs); playbackInfo.positionUs = positionUs; playbackInfo.bufferedPositionUs = bufferedPositionUs; return playbackInfo; @@ -112,6 +127,7 @@ import java.io.IOException; private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; private static final int MSG_CUSTOM = 11; + private static final int MSG_SET_REPEAT_MODE = 12; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -143,6 +159,7 @@ import java.io.IOException; private final ExoPlayer player; private final Timeline.Window window; private final Timeline.Period period; + private final MediaPeriodInfoSequence mediaPeriodInfoSequence; private PlaybackInfo playbackInfo; private PlaybackParameters playbackParameters; @@ -155,6 +172,7 @@ import java.io.IOException; private boolean rebuffering; private boolean isLoading; private int state; + private @Player.RepeatMode int repeatMode; private int customMessagesSent; private int customMessagesProcessed; private long elapsedRealtimeUs; @@ -170,14 +188,15 @@ import java.io.IOException; private Timeline timeline; public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, Handler eventHandler, - PlaybackInfo playbackInfo, ExoPlayer player) { + LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode, + Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; this.loadControl = loadControl; this.playWhenReady = playWhenReady; + this.repeatMode = repeatMode; this.eventHandler = eventHandler; - this.state = ExoPlayer.STATE_IDLE; + this.state = Player.STATE_IDLE; this.playbackInfo = playbackInfo; this.player = player; @@ -190,6 +209,7 @@ import java.io.IOException; enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); + mediaPeriodInfoSequence = new MediaPeriodInfoSequence(); trackSelector.init(this); playbackParameters = PlaybackParameters.DEFAULT; @@ -210,6 +230,10 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } + public void setRepeatMode(@Player.RepeatMode int repeatMode) { + handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget(); + } + public void seekTo(Timeline timeline, int windowIndex, long positionUs) { handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) .sendToTarget(); @@ -263,6 +287,10 @@ import java.io.IOException; internalPlaybackThread.quit(); } + public Looper getPlaybackLooper() { + return internalPlaybackThread.getLooper(); + } + // MediaSource.Listener implementation. @Override @@ -304,6 +332,10 @@ import java.io.IOException; setPlayWhenReadyInternal(msg.arg1 != 0); return true; } + case MSG_SET_REPEAT_MODE: { + setRepeatModeInternal(msg.arg1); + return true; + } case MSG_DO_SOME_WORK: { doSomeWork(); return true; @@ -391,7 +423,7 @@ import java.io.IOException; } this.mediaSource = mediaSource; mediaSource.prepareSource(player, true, this); - setState(ExoPlayer.STATE_BUFFERING); + setState(Player.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -402,15 +434,69 @@ import java.io.IOException; stopRenderers(); updatePlaybackPositions(); } else { - if (state == ExoPlayer.STATE_READY) { + if (state == Player.STATE_READY) { startRenderers(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else if (state == ExoPlayer.STATE_BUFFERING) { + } else if (state == Player.STATE_BUFFERING) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } } + private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) + throws ExoPlaybackException { + this.repeatMode = repeatMode; + mediaPeriodInfoSequence.setRepeatMode(repeatMode); + + // Find the last existing period holder that matches the new period order. + MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null + ? playingPeriodHolder : loadingPeriodHolder; + if (lastValidPeriodHolder == null) { + return; + } + while (true) { + int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex, + period, window, repeatMode); + while (lastValidPeriodHolder.next != null + && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { + lastValidPeriodHolder = lastValidPeriodHolder.next; + } + if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null + || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) { + break; + } + lastValidPeriodHolder = lastValidPeriodHolder.next; + } + + // Release any period holders that don't match the new period order. + int loadingPeriodHolderIndex = loadingPeriodHolder.index; + int readingPeriodHolderIndex = + readingPeriodHolder != null ? readingPeriodHolder.index : C.INDEX_UNSET; + if (lastValidPeriodHolder.next != null) { + releasePeriodHoldersFrom(lastValidPeriodHolder.next); + lastValidPeriodHolder.next = null; + } + + // Update the period info for the last holder, as it may now be the last period in the timeline. + lastValidPeriodHolder.info = + mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); + + // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. + boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index; + if (!seenLoadingPeriodHolder) { + loadingPeriodHolder = lastValidPeriodHolder; + } + boolean seenReadingPeriodHolder = readingPeriodHolderIndex != C.INDEX_UNSET + && readingPeriodHolderIndex <= lastValidPeriodHolder.index; + if (!seenReadingPeriodHolder && playingPeriodHolder != null) { + // Renderers may have read from a period that's been removed. Seek back to the current + // position of the playing period to make sure none of the removed period is played. + MediaPeriodId periodId = playingPeriodHolder.info.id; + long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); + playbackInfo = new PlaybackInfo(periodId, newPositionUs, playbackInfo.contentPositionUs); + } + } + private void startRenderers() throws ExoPlaybackException { rebuffering = false; standaloneMediaClock.start(); @@ -451,8 +537,7 @@ import java.io.IOException; long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE : playingPeriodHolder.mediaPeriod.getBufferedPositionUs(); playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE - ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs() - : bufferedPositionUs; + ? playingPeriodHolder.info.durationUs : bufferedPositionUs; } private void doSomeWork() throws ExoPlaybackException, IOException { @@ -504,43 +589,43 @@ import java.io.IOException; } } - long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period) - .getDurationUs(); + long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) - && playingPeriodHolder.isLast) { - setState(ExoPlayer.STATE_ENDED); + && playingPeriodHolder.info.isFinal) { + setState(Player.STATE_ENDED); stopRenderers(); - } else if (state == ExoPlayer.STATE_BUFFERING) { + } else if (state == Player.STATE_BUFFERING) { boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering)) + ? (allRenderersReadyOrEnded + && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs)) : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { - setState(ExoPlayer.STATE_READY); + setState(Player.STATE_READY); if (playWhenReady) { startRenderers(); } } - } else if (state == ExoPlayer.STATE_READY) { + } else if (state == Player.STATE_READY) { boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded : isTimelineReady(playingPeriodDurationUs); if (!isStillReady) { rebuffering = playWhenReady; - setState(ExoPlayer.STATE_BUFFERING); + setState(Player.STATE_BUFFERING); stopRenderers(); } } - if (state == ExoPlayer.STATE_BUFFERING) { + if (state == Player.STATE_BUFFERING) { for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } } - if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) { + if ((playWhenReady && state == Player.STATE_READY) || state == Player.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); - } else if (enabledRenderers.length != 0) { + } else if (enabledRenderers.length != 0 && state != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -576,7 +661,7 @@ import java.io.IOException; // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't // ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); - setState(ExoPlayer.STATE_ENDED); + setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); return; @@ -585,28 +670,34 @@ import java.io.IOException; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; - + long contentPositionUs = periodPositionUs; + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs); + if (periodId.isAd()) { + seekPositionAdjusted = true; + periodPositionUs = 0; + } try { - if (periodIndex == playbackInfo.periodIndex + if (periodId.equals(playbackInfo.periodId) && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { // Seek position equals the current position. Do nothing. return; } - long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } finally { - playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); + playbackInfo = new PlaybackInfo(periodId, periodPositionUs, contentPositionUs); eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) .sendToTarget(); } } - private long seekToPeriodPosition(int periodIndex, long periodPositionUs) + private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) throws ExoPlaybackException { stopRenderers(); rebuffering = false; - setState(ExoPlayer.STATE_BUFFERING); + setState(Player.STATE_BUFFERING); MediaPeriodHolder newPlayingPeriodHolder = null; if (playingPeriodHolder == null) { @@ -618,7 +709,7 @@ import java.io.IOException; // Clear the timeline, but keep the requested period if it is already prepared. MediaPeriodHolder periodHolder = playingPeriodHolder; while (periodHolder != null) { - if (periodHolder.index == periodIndex && periodHolder.prepared) { + if (shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { newPlayingPeriodHolder = periodHolder; } else { periodHolder.release(); @@ -662,6 +753,19 @@ import java.io.IOException; return periodPositionUs; } + private boolean shouldKeepPeriodHolder(MediaPeriodId seekPeriodId, long positionUs, + MediaPeriodHolder holder) { + if (seekPeriodId.equals(holder.info.id) && holder.prepared) { + timeline.getPeriod(holder.info.id.periodIndex, period); + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); + if (nextAdGroupIndex == C.INDEX_UNSET + || period.getAdGroupTimeUs(nextAdGroupIndex) == holder.info.endPositionUs) { + return true; + } + } + return false; + } + private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US @@ -683,13 +787,13 @@ import java.io.IOException; private void stopInternal() { resetInternal(true); loadControl.onStopped(); - setState(ExoPlayer.STATE_IDLE); + setState(Player.STATE_IDLE); } private void releaseInternal() { resetInternal(true); loadControl.onReleased(); - setState(ExoPlayer.STATE_IDLE); + setState(Player.STATE_IDLE); synchronized (this) { released = true; notifyAll(); @@ -724,6 +828,7 @@ import java.io.IOException; mediaSource.releaseSource(); mediaSource = null; } + mediaPeriodInfoSequence.setTimeline(null); timeline = null; } } @@ -733,7 +838,7 @@ import java.io.IOException; for (ExoPlayerMessage message : messages) { message.target.handleMessage(message.messageType, message.message); } - if (mediaSource != null) { + if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -834,7 +939,7 @@ import java.io.IOException; } loadingPeriodHolder.next = null; if (loadingPeriodHolder.prepared) { - long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs, + long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.info.startPositionUs, loadingPeriodHolder.toPeriodTime(rendererPositionUs)); loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } @@ -847,23 +952,8 @@ import java.io.IOException; private boolean isTimelineReady(long playingPeriodDurationUs) { return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs - || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared); - } - - private boolean haveSufficientBuffer(boolean rebuffering) { - long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared - ? loadingPeriodHolder.startPositionUs - : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); - if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) { - if (loadingPeriodHolder.isLast) { - return true; - } - loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period) - .getDurationUs(); - } - return loadControl.shouldStartPlayback( - loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs), - rebuffering); + || (playingPeriodHolder.next != null + && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd())); } private void maybeThrowPeriodPrepareError() throws IOException { @@ -882,6 +972,7 @@ import java.io.IOException; throws ExoPlaybackException { Timeline oldTimeline = timeline; timeline = timelineAndManifest.first; + mediaPeriodInfoSequence.setTimeline(timeline); Object manifest = timelineAndManifest.second; int processedInitialSeekCount = 0; @@ -895,32 +986,45 @@ import java.io.IOException; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; + } else { + int periodIndex = periodPosition.first; + long positionUs = periodPosition.second; + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); } - playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; + } else { + Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); + int periodIndex = defaultPosition.first; + long startPositionUs = defaultPosition.second; + MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, + startPositionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, + startPositionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); } - Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); - playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second); } - } - - MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder; - if (periodHolder == null) { - // We don't have any period holders, so we're done. - notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } - int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid); + int playingPeriodIndex = playbackInfo.periodId.periodIndex; + MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder + : loadingPeriodHolder; + if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { + notifySourceInfoRefresh(manifest, processedInitialSeekCount); + return; + } + Object playingPeriodUid = periodHolder == null + ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid; + int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid); if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent // period whose window we can restart from. - int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline); + int newPeriodIndex = resolveSubsequentPeriod(playingPeriodIndex, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); @@ -932,52 +1036,75 @@ import java.io.IOException; newPeriodIndex = defaultPosition.first; long newPositionUs = defaultPosition.second; timeline.getPeriod(newPeriodIndex, period, true); - // Clear the index of each holder that doesn't contain the default position. If a holder - // contains the default position then update its index so it can be re-used when seeking. - Object newPeriodUid = period.uid; - periodHolder.index = C.INDEX_UNSET; - while (periodHolder.next != null) { - periodHolder = periodHolder.next; - periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET; + if (periodHolder != null) { + // Clear the index of each holder that doesn't contain the default position. If a holder + // contains the default position then update its index so it can be re-used when seeking. + Object newPeriodUid = period.uid; + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + while (periodHolder.next != null) { + periodHolder = periodHolder.next; + if (periodHolder.uid.equals(newPeriodUid)) { + periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, + newPeriodIndex); + } else { + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + } + } } // Actually do the seek. - newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs); - playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs); + MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); + newPositionUs = seekToPeriodPosition(periodId, newPositionUs); + playbackInfo = new PlaybackInfo(periodId, newPositionUs); notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } - // The current period is in the new timeline. Update the holder and playbackInfo. - timeline.getPeriod(periodIndex, period); - boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - periodHolder.setIndex(periodIndex, isLastPeriod); - boolean seenReadingPeriod = periodHolder == readingPeriodHolder; - if (periodIndex != playbackInfo.periodIndex) { + // The current period is in the new timeline. Update the playback info. + if (periodIndex != playingPeriodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); } - // If there are subsequent holders, update the index for each of them. If we find a holder - // that's inconsistent with the new timeline then take appropriate action. + if (playbackInfo.periodId.isAd()) { + // Check that the playing ad hasn't been marked as played. If it has, skip forward. + MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, + playbackInfo.contentPositionUs); + if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) { + long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); + long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; + playbackInfo = new PlaybackInfo(periodId, newPositionUs, contentPositionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); + return; + } + } + + if (periodHolder == null) { + // We don't have any period holders, so we're done. + notifySourceInfoRefresh(manifest, processedInitialSeekCount); + return; + } + + // Update the holder indices. If we find a subsequent holder that's inconsistent with the new + // timeline then take appropriate action. + periodHolder = updatePeriodInfo(periodHolder, periodIndex); while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex++; - timeline.getPeriod(periodIndex, period, true); - isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; - if (periodHolder.uid.equals(period.uid)) { + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode); + if (periodIndex != C.INDEX_UNSET + && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. - periodHolder.setIndex(periodIndex, isLastPeriod); - seenReadingPeriod |= (periodHolder == readingPeriodHolder); + periodHolder = updatePeriodInfo(periodHolder, periodIndex); } else { // The holder is inconsistent with the new timeline. - if (!seenReadingPeriod) { + boolean seenReadingPeriodHolder = + readingPeriodHolder != null && readingPeriodHolder.index < periodHolder.index; + if (!seenReadingPeriodHolder) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. - periodIndex = playingPeriodHolder.index; - long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs); - playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); + long newPositionUs = + seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs); + playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, newPositionUs, + playbackInfo.contentPositionUs); } else { // Update the loading period to be the last period that's still valid, and release all // subsequent periods. @@ -993,6 +1120,17 @@ import java.io.IOException; notifySourceInfoRefresh(manifest, processedInitialSeekCount); } + private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { + while (true) { + periodHolder.info = + mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); + if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) { + return periodHolder; + } + periodHolder = periodHolder.next; + } + } + private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { // Set the playback position to (0,0) for notifying the eventHandler. @@ -1000,7 +1138,7 @@ import java.io.IOException; notifySourceInfoRefresh(manifest, processedInitialSeekCount); // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); - setState(ExoPlayer.STATE_ENDED); + setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); } @@ -1023,9 +1161,15 @@ import java.io.IOException; private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { int newPeriodIndex = C.INDEX_UNSET; - while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { + int maxIterations = oldTimeline.getPeriodCount(); + for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { + oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode); + if (oldPeriodIndex == C.INDEX_UNSET) { + // We've reached the end of the old timeline. + break; + } newPeriodIndex = newTimeline.getIndexOfPeriod( - oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid); + oldTimeline.getPeriod(oldPeriodIndex, period, true).uid); } return newPeriodIndex; } @@ -1049,7 +1193,7 @@ import java.io.IOException; // Map the SeekPosition to a position in the corresponding timeline. Pair periodPosition; try { - periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex, + periodPosition = seekTimeline.getPeriodPosition(window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); } catch (IndexOutOfBoundsException e) { // The window index of the seek position was outside the bounds of the timeline. @@ -1078,53 +1222,11 @@ import java.io.IOException; } /** - * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline. + * Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the + * current timeline. */ private Pair getPeriodPosition(int windowIndex, long windowPositionUs) { - return getPeriodPosition(timeline, windowIndex, windowPositionUs); - } - - /** - * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position - * projection. - */ - private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs) { - return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0); - } - - /** - * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). - * - * @param timeline The timeline containing the window. - * @param windowIndex The window index. - * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default - * start position. - * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the - * duration into the future by which the window's position should be projected. - * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} - * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's - * position could not be projected by {@code defaultPositionProjectionUs}. - */ - private Pair getPeriodPosition(Timeline timeline, int windowIndex, - long windowPositionUs, long defaultPositionProjectionUs) { - Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount()); - timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs); - if (windowPositionUs == C.TIME_UNSET) { - windowPositionUs = window.getDefaultPositionUs(); - if (windowPositionUs == C.TIME_UNSET) { - return null; - } - } - int periodIndex = window.firstPeriodIndex; - long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; - long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); - while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs - && periodIndex < window.lastPeriodIndex) { - periodPositionUs -= periodDurationUs; - periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); - } - return Pair.create(periodIndex, periodPositionUs); + return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); } private void updatePeriods() throws ExoPlaybackException, IOException { @@ -1138,7 +1240,7 @@ import java.io.IOException; maybeUpdateLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); - } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) { + } else if (loadingPeriodHolder != null && !isLoading) { maybeContinueLoading(); } @@ -1154,13 +1256,13 @@ import java.io.IOException; // the end of the playing period, so advance playback to the next period. playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); - playbackInfo = new PlaybackInfo(playingPeriodHolder.index, - playingPeriodHolder.startPositionUs); + playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, + playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); updatePlaybackPositions(); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } - if (readingPeriodHolder.isLast) { + if (readingPeriodHolder.info.isFinal) { for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; @@ -1224,76 +1326,41 @@ import java.io.IOException; } private void maybeUpdateLoadingPeriod() throws IOException { - int newLoadingPeriodIndex; + MediaPeriodInfo info; if (loadingPeriodHolder == null) { - newLoadingPeriodIndex = playbackInfo.periodIndex; + info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo); } else { - int loadingPeriodIndex = loadingPeriodHolder.index; - if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered() - || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { - // Either the existing loading period is the last period, or we are not ready to advance to - // loading the next period because it hasn't been fully buffered or its duration is unknown. + if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered() + || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) { return; } - if (playingPeriodHolder != null - && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) { - // We are already buffering the maximum number of periods ahead. - return; + if (playingPeriodHolder != null) { + int bufferAheadPeriodCount = loadingPeriodHolder.index - playingPeriodHolder.index; + if (bufferAheadPeriodCount == MAXIMUM_BUFFER_AHEAD_PERIODS) { + // We are already buffering the maximum number of periods ahead. + return; + } } - newLoadingPeriodIndex = loadingPeriodHolder.index + 1; + info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info, + loadingPeriodHolder.getRendererOffset(), rendererPositionUs); } - - if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { - // The next period is not available yet. + if (info == null) { mediaSource.maybeThrowSourceInfoRefreshError(); return; } - long newLoadingPeriodStartPositionUs; - if (loadingPeriodHolder == null) { - newLoadingPeriodStartPositionUs = playbackInfo.positionUs; - } else { - int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; - if (newLoadingPeriodIndex - != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) { - // We're starting to buffer a new period in the current window. Always start from the - // beginning of the period. - newLoadingPeriodStartPositionUs = 0; - } else { - // We're starting to buffer a new window. When playback transitions to this window we'll - // want it to be from its default start position. The expected delay until playback - // transitions is equal the duration of media that's currently buffered (assuming no - // interruptions). Hence we project the default start position forward by the duration of - // the buffer, and start buffering from this point. - long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - - rendererPositionUs; - Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex, - C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); - if (defaultPosition == null) { - return; - } - - newLoadingPeriodIndex = defaultPosition.first; - newLoadingPeriodStartPositionUs = defaultPosition.second; - } - } - long rendererPositionOffsetUs = loadingPeriodHolder == null - ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US - : (loadingPeriodHolder.getRendererOffset() - + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); - timeline.getPeriod(newLoadingPeriodIndex, period, true); - boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 - && !timeline.getWindow(period.windowIndex, window).isDynamic; + ? RENDERER_TIMESTAMP_OFFSET_US + : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); + int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; + Object uid = timeline.getPeriod(info.id.periodIndex, period, true).uid; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, - newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); + rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, uid, holderIndex, info); if (loadingPeriodHolder != null) { loadingPeriodHolder.next = newPeriodHolder; } loadingPeriodHolder = newPeriodHolder; - loadingPeriodHolder.mediaPeriod.prepare(this); + loadingPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); } @@ -1306,7 +1373,7 @@ import java.io.IOException; if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; - resetRendererPosition(readingPeriodHolder.startPositionUs); + resetRendererPosition(readingPeriodHolder.info.startPositionUs); setPlayingPeriodHolder(readingPeriodHolder); } maybeContinueLoading(); @@ -1321,21 +1388,10 @@ import java.io.IOException; } private void maybeContinueLoading() { - long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0 - : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { - setIsLoading(false); - } else { - long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs); - long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; - boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs); - setIsLoading(continueLoading); - if (continueLoading) { - loadingPeriodHolder.needsContinueLoading = false; - loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs); - } else { - loadingPeriodHolder.needsContinueLoading = true; - } + boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs); + setIsLoading(continueLoading); + if (continueLoading) { + loadingPeriodHolder.continueLoading(rendererPositionUs); } } @@ -1395,7 +1451,7 @@ import java.io.IOException; RendererConfiguration rendererConfiguration = playingPeriodHolder.trackSelectorResult.rendererConfigurations[i]; // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; + boolean playing = playWhenReady && state == Player.STATE_READY; // Consider as joining only if the renderer was previously disabled. boolean joining = !rendererWasEnabledFlags[i] && playing; // Build an array of formats contained by the selection. @@ -1432,17 +1488,15 @@ import java.io.IOException; public final MediaPeriod mediaPeriod; public final Object uid; + public final int index; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; public final long rendererPositionOffsetUs; - public int index; - public long startPositionUs; - public boolean isLast; + public MediaPeriodInfo info; public boolean prepared; public boolean hasEnabledTracks; public MediaPeriodHolder next; - public boolean needsContinueLoading; public TrackSelectorResult trackSelectorResult; private final Renderer[] renderers; @@ -1455,8 +1509,7 @@ import java.io.IOException; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, - MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod, - long startPositionUs) { + MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; @@ -1464,13 +1517,17 @@ import java.io.IOException; this.loadControl = loadControl; this.mediaSource = mediaSource; this.uid = Assertions.checkNotNull(periodUid); - this.index = periodIndex; - this.isLast = isLastPeriod; - this.startPositionUs = startPositionUs; + this.index = index; + this.info = info; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; - mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(), - startPositionUs); + MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, loadControl.getAllocator()); + if (info.endPositionUs != C.TIME_END_OF_SOURCE) { + ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true); + clippingMediaPeriod.setClipping(0, info.endPositionUs); + mediaPeriod = clippingMediaPeriod; + } + this.mediaPeriod = mediaPeriod; } public long toRendererTime(long periodTimeUs) { @@ -1482,12 +1539,8 @@ import java.io.IOException; } public long getRendererOffset() { - return rendererPositionOffsetUs - startPositionUs; - } - - public void setIndex(int index, boolean isLast) { - this.index = index; - this.isLast = isLast; + return index == 0 ? rendererPositionOffsetUs + : (rendererPositionOffsetUs - info.startPositionUs); } public boolean isFullyBuffered() { @@ -1495,10 +1548,40 @@ import java.io.IOException; && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } + public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) { + long bufferedPositionUs = !prepared ? info.startPositionUs + : mediaPeriod.getBufferedPositionUs(); + if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { + if (info.isFinal) { + return true; + } + bufferedPositionUs = info.durationUs; + } + return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs), + rebuffering); + } + public void handlePrepared() throws ExoPlaybackException { prepared = true; selectTracks(); - startPositionUs = updatePeriodTrackSelection(startPositionUs, false); + long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); + info = info.copyWithStartPositionUs(newStartPositionUs); + } + + public boolean shouldContinueLoading(long rendererPositionUs) { + long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { + return false; + } else { + long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); + long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; + return loadControl.shouldContinueLoading(bufferedDurationUs); + } + } + + public void continueLoading(long rendererPositionUs) { + long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); + mediaPeriod.continueLoading(loadingPeriodPositionUs); } public boolean selectTracks() throws ExoPlaybackException { @@ -1547,7 +1630,11 @@ import java.io.IOException; public void release() { try { - mediaSource.releasePeriod(mediaPeriod); + if (info.endPositionUs != C.TIME_END_OF_SOURCE) { + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + } else { + mediaSource.releasePeriod(mediaPeriod); + } } catch (RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Period release failed.", e); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 73d91f293e..fd5ead5c85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -15,22 +15,29 @@ */ package com.google.android.exoplayer2; +import java.util.HashSet; + /** * Information about the ExoPlayer library. */ -public interface ExoPlayerLibraryInfo { +public final class ExoPlayerLibraryInfo { + + /** + * A tag to use when logging library information. + */ + public static final String TAG = "ExoPlayer"; /** * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - String VERSION = "2.4.4"; + public static final String VERSION = "2.5.0"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - String VERSION_SLASHY = "ExoPlayerLib/2.4.4"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,18 +47,41 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004004; + public static final int VERSION_INT = 2005000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * checks enabled. */ - boolean ASSERTIONS_ENABLED = true; + public static final boolean ASSERTIONS_ENABLED = true; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil} * trace enabled. */ - boolean TRACE_ENABLED = true; + public static final boolean TRACE_ENABLED = true; + + private static final HashSet registeredModules = new HashSet<>(); + private static String registeredModulesString = "goog.exo.core"; + + private ExoPlayerLibraryInfo() {} // Prevents instantiation. + + /** + * Returns a string consisting of registered module names separated by ", ". + */ + public static synchronized String registeredModules() { + return registeredModulesString; + } + + /** + * Registers a module to be returned in the {@link #registeredModules()} string. + * + * @param name The name of the module being registered. + */ + public static synchronized void registerModule(String name) { + if (registeredModules.add(name)) { + registeredModulesString = registeredModulesString + ", " + name; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 0bffd28ba5..4e387ac7ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -286,9 +286,14 @@ public final class Format implements Parcelable { OFFSET_SAMPLE_RELATIVE, null, null, null); } - public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, - int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) { - return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, + public static Format createTextSampleFormat(String id, String sampleMimeType, + @C.SelectionFlags int selectionFlags, String language) { + return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null); + } + + public static Format createTextSampleFormat(String id, String sampleMimeType, + @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) { + return createTextSampleFormat(id, sampleMimeType, null, NO_VALUE, selectionFlags, language, NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.emptyList()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java new file mode 100644 index 0000000000..0e9c65421c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -0,0 +1,352 @@ +/* + * 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 android.util.Pair; +import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; +import com.google.android.exoplayer2.Player.RepeatMode; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; + +/** + * Provides a sequence of {@link MediaPeriodInfo}s to the player, determining the order and + * start/end positions for {@link MediaPeriod}s to load and play. + */ +/* package */ final class MediaPeriodInfoSequence { + + // TODO: Consider merging this class with the MediaPeriodHolder queue in ExoPlayerImplInternal. + + /** + * Stores the information required to load and play a {@link MediaPeriod}. + */ + public static final class MediaPeriodInfo { + + /** + * The media period's identifier. + */ + public final MediaPeriodId id; + /** + * The start position of the media to play within the media period, in microseconds. + */ + public final long startPositionUs; + /** + * The end position of the media to play within the media period, in microseconds, or + * {@link C#TIME_END_OF_SOURCE} if the end position is the end of the media period. + */ + public final long endPositionUs; + /** + * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} + * otherwise. + */ + public final long contentPositionUs; + /** + * The duration of the media to play within the media period, in microseconds, or + * {@link C#TIME_UNSET} if not known. + */ + public final long durationUs; + /** + * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media + * period corresponding to a timeline period without ads). + */ + public final boolean isLastInTimelinePeriod; + /** + * Whether this is the last media period in the entire timeline. If true, + * {@link #isLastInTimelinePeriod} will also be true. + */ + public final boolean isFinal; + + private MediaPeriodInfo(MediaPeriodId id, long startPositionUs, long endPositionUs, + long contentPositionUs, long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) { + this.id = id; + this.startPositionUs = startPositionUs; + this.endPositionUs = endPositionUs; + this.contentPositionUs = contentPositionUs; + this.durationUs = durationUs; + this.isLastInTimelinePeriod = isLastInTimelinePeriod; + this.isFinal = isFinal; + } + + /** + * Returns a copy of this instance with the period identifier's period index set to the + * specified value. + */ + public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) { + return new MediaPeriodInfo(id.copyWithPeriodIndex(periodIndex), startPositionUs, + endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); + } + + /** + * Returns a copy of this instance with the start position set to the specified value. + */ + public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { + return new MediaPeriodInfo(id, startPositionUs, endPositionUs, contentPositionUs, durationUs, + isLastInTimelinePeriod, isFinal); + } + + } + + private final Timeline.Period period; + private final Timeline.Window window; + + private Timeline timeline; + @RepeatMode + private int repeatMode; + + /** + * Creates a new media period info sequence. + */ + public MediaPeriodInfoSequence() { + period = new Timeline.Period(); + window = new Timeline.Window(); + } + + /** + * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information + * taking into account the new timeline. + */ + public void setTimeline(Timeline timeline) { + this.timeline = timeline; + } + + /** + * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period + * information taking into account the new repeat mode. + */ + public void setRepeatMode(@RepeatMode int repeatMode) { + this.repeatMode = repeatMode; + } + + /** + * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position. + */ + public MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { + return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.contentPositionUs, + playbackInfo.startPositionUs); + } + + /** + * Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}. + * + * @param currentMediaPeriodInfo The current media period info. + * @param rendererOffsetUs The current renderer offset in microseconds. + * @param rendererPositionUs The current renderer position in microseconds. + * @return The following media period info, or {@code null} if it is not yet possible to get the + * next media period info. + */ + public MediaPeriodInfo getNextMediaPeriodInfo(MediaPeriodInfo currentMediaPeriodInfo, + long rendererOffsetUs, long rendererPositionUs) { + // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod + // but if the timeline is not ready to provide the next period it can't return a non-null value + // until the timeline is updated. Store whether the next timeline period is ready when the + // timeline is updated, to avoid repeatedly checking the same timeline. + if (currentMediaPeriodInfo.isLastInTimelinePeriod) { + int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, + period, window, repeatMode); + if (nextPeriodIndex == C.INDEX_UNSET) { + // We can't create a next period yet. + return null; + } + + long startPositionUs; + int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex; + if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) { + // We're starting to buffer a new window. When playback transitions to this window we'll + // want it to be from its default start position. The expected delay until playback + // transitions is equal the duration of media that's currently buffered (assuming no + // interruptions). Hence we project the default start position forward by the duration of + // the buffer, and start buffering from this point. + long defaultPositionProjectionUs = + rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs; + Pair defaultPosition = timeline.getPeriodPosition(window, period, + nextWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); + if (defaultPosition == null) { + return null; + } + nextPeriodIndex = defaultPosition.first; + startPositionUs = defaultPosition.second; + } else { + startPositionUs = 0; + } + MediaPeriodId periodId = resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs); + return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); + } + + MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id; + if (currentPeriodId.isAd()) { + int currentAdGroupIndex = currentPeriodId.adGroupIndex; + timeline.getPeriod(currentPeriodId.periodIndex, period); + int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex); + if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { + return null; + } + int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1; + if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) { + // Play the next ad in the ad group if it's available. + return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) ? null + : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, currentAdGroupIndex, + nextAdIndexInAdGroup, currentMediaPeriodInfo.contentPositionUs); + } else { + // Play content from the ad group position. + int nextAdGroupIndex = + period.getAdGroupIndexAfterPositionUs(currentMediaPeriodInfo.contentPositionUs); + long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE + : period.getAdGroupTimeUs(nextAdGroupIndex); + return getMediaPeriodInfoForContent(currentPeriodId.periodIndex, + currentMediaPeriodInfo.contentPositionUs, endUs); + } + } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { + // Play the next ad group if it's available. + int nextAdGroupIndex = + period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs); + return !period.isAdAvailable(nextAdGroupIndex, 0) ? null + : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0, + currentMediaPeriodInfo.endPositionUs); + } else { + // Check if the postroll ad should be played. + int adGroupCount = period.getAdGroupCount(); + if (adGroupCount == 0 + || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE + || period.hasPlayedAdGroup(adGroupCount - 1) + || !period.isAdAvailable(adGroupCount - 1, 0)) { + return null; + } + long contentDurationUs = period.getDurationUs(); + return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0, + contentDurationUs); + } + } + + /** + * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be + * played, returning an identifier for an ad group if one needs to be played before the specified + * position, or an identifier for a content media period if not. + */ + public MediaPeriodId resolvePeriodPositionForAds(int periodIndex, long positionUs) { + timeline.getPeriod(periodIndex, period); + int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); + if (adGroupIndex == C.INDEX_UNSET) { + return new MediaPeriodId(periodIndex); + } else { + int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex); + return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); + } + } + + /** + * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline. + */ + public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo) { + return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id); + } + + /** + * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline, + * resetting the identifier of the media period to the specified {@code newPeriodIndex}. + */ + public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo, + int newPeriodIndex) { + return getUpdatedMediaPeriodInfo(mediaPeriodInfo, + mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex)); + } + + // Internal methods. + + private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) { + long startPositionUs = info.startPositionUs; + long endPositionUs = info.endPositionUs; + boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs); + boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod); + timeline.getPeriod(newId.periodIndex, period); + long durationUs = newId.isAd() + ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup) + : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs); + return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, info.contentPositionUs, + durationUs, isLastInPeriod, isLastInTimeline); + } + + private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long contentPositionUs, + long startPositionUs) { + timeline.getPeriod(id.periodIndex, period); + if (id.isAd()) { + if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { + return null; + } + return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, + contentPositionUs); + } else { + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); + long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE + : period.getAdGroupTimeUs(nextAdGroupIndex); + return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs); + } + } + + private MediaPeriodInfo getMediaPeriodInfoForAd(int periodIndex, int adGroupIndex, + int adIndexInAdGroup, long contentPositionUs) { + MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); + boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + long durationUs = timeline.getPeriod(id.periodIndex, period) + .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); + long startPositionUs = adIndexInAdGroup == period.getPlayedAdCount(adGroupIndex) + ? period.getAdResumePositionUs() : 0; + return new MediaPeriodInfo(id, startPositionUs, C.TIME_END_OF_SOURCE, contentPositionUs, + durationUs, isLastInPeriod, isLastInTimeline); + } + + private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs, + long endUs) { + MediaPeriodId id = new MediaPeriodId(periodIndex); + boolean isLastInPeriod = isLastInPeriod(id, endUs); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); + timeline.getPeriod(id.periodIndex, period); + long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; + return new MediaPeriodInfo(id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, + isLastInTimeline); + } + + private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) { + int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount(); + if (adGroupCount == 0) { + return true; + } + + int lastAdGroupIndex = adGroupCount - 1; + boolean isAd = id.isAd(); + if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) { + // There's no postroll ad. + return !isAd && endPositionUs == C.TIME_END_OF_SOURCE; + } + + int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex); + if (postrollAdCount == C.LENGTH_UNSET) { + // We won't know if this is the last ad until we know how many postroll ads there are. + return false; + } + + boolean isLastAd = isAd && id.adGroupIndex == lastAdGroupIndex + && id.adIndexInAdGroup == postrollAdCount - 1; + return isLastAd || (!isAd && period.getPlayedAdCount(lastAdGroupIndex) == postrollAdCount); + } + + private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { + int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; + return !timeline.getWindow(windowIndex, window).isDynamic + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode) + && isLastMediaPeriodInPeriod; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java new file mode 100644 index 0000000000..d2480c5b3a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -0,0 +1,410 @@ +/* + * 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 android.os.Looper; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A media player interface defining traditional high-level functionality, such as the ability to + * play, pause, seek and query properties of the currently playing media. + *

+ * Some important properties of media players that implement this interface are: + *

+ */ +public interface Player { + + /** + * Listener of changes in player state. + */ + interface EventListener { + + /** + * Called when the timeline and/or manifest has been refreshed. + *

+ * Note that if the timeline has changed then a position discontinuity may also have occurred. + * For example, the current period index may have changed as a result of periods being added or + * removed from the timeline. This will not be reported via a separate call to + * {@link #onPositionDiscontinuity()}. + * + * @param timeline The latest timeline. Never null, but may be empty. + * @param manifest The latest manifest. May be null. + */ + void onTimelineChanged(Timeline timeline, Object manifest); + + /** + * Called when the available or selected tracks change. + * + * @param trackGroups The available tracks. Never null, but may be of length zero. + * @param trackSelections The track selections for each renderer. Never null and always of + * length {@link #getRendererCount()}, but may contain null elements. + */ + void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections); + + /** + * Called when the player starts or stops loading the source. + * + * @param isLoading Whether the source is currently being loaded. + */ + void onLoadingChanged(boolean isLoading); + + /** + * Called when the value returned from either {@link #getPlayWhenReady()} or + * {@link #getPlaybackState()} changes. + * + * @param playWhenReady Whether playback will proceed when ready. + * @param playbackState One of the {@code STATE} constants. + */ + void onPlayerStateChanged(boolean playWhenReady, int playbackState); + + /** + * Called when the value of {@link #getRepeatMode()} changes. + * + * @param repeatMode The {@link RepeatMode} used for playback. + */ + void onRepeatModeChanged(@RepeatMode int repeatMode); + + /** + * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} + * immediately after this method is called. The player instance can still be used, and + * {@link #release()} must still be called on the player should it no longer be required. + * + * @param error The error. + */ + void onPlayerError(ExoPlaybackException error); + + /** + * Called when a position discontinuity occurs without a change to the timeline. A position + * discontinuity occurs when the current window or period index changes (as a result of playback + * transitioning from one period in the timeline to the next), or when the playback position + * jumps within the period currently being played (as a result of a seek being performed, or + * when the source introduces a discontinuity internally). + *

+ * When a position discontinuity occurs as a result of a change to the timeline this method is + * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. + */ + void onPositionDiscontinuity(); + + /** + * Called when the current playback parameters change. The playback parameters may change due to + * a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change + * them (for example, if audio playback switches to passthrough mode, where speed adjustment is + * no longer possible). + * + * @param playbackParameters The playback parameters. + */ + void onPlaybackParametersChanged(PlaybackParameters playbackParameters); + + } + + /** + * The player does not have any media to play. + */ + int STATE_IDLE = 1; + /** + * The player is not able to immediately play from its current position. This state typically + * occurs when more data needs to be loaded. + */ + int STATE_BUFFERING = 2; + /** + * The player is able to immediately play from its current position. The player will be playing if + * {@link #getPlayWhenReady()} is true, and paused otherwise. + */ + int STATE_READY = 3; + /** + * The player has finished playing the media. + */ + int STATE_ENDED = 4; + + /** + * Repeat modes for playback. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL}) + public @interface RepeatMode {} + /** + * Normal playback without repetition. + */ + int REPEAT_MODE_OFF = 0; + /** + * "Repeat One" mode to repeat the currently playing window infinitely. + */ + int REPEAT_MODE_ONE = 1; + /** + * "Repeat All" mode to repeat the entire timeline infinitely. + */ + int REPEAT_MODE_ALL = 2; + + /** + * Register a listener to receive events from the player. The listener's methods will be called on + * the thread that was used to construct the player. However, if the thread used to construct the + * player does not have a {@link Looper}, then the listener will be called on the main thread. + * + * @param listener The listener to register. + */ + void addListener(EventListener listener); + + /** + * Unregister a listener. The listener will no longer receive events from the player. + * + * @param listener The listener to unregister. + */ + void removeListener(EventListener listener); + + /** + * Returns the current state of the player. + * + * @return One of the {@code STATE} constants defined in this interface. + */ + int getPlaybackState(); + + /** + * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. + *

+ * If the player is already in the ready state then this method can be used to pause and resume + * playback. + * + * @param playWhenReady Whether playback should proceed when ready. + */ + void setPlayWhenReady(boolean playWhenReady); + + /** + * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. + * + * @return Whether playback will proceed when ready. + */ + boolean getPlayWhenReady(); + + /** + * Sets the {@link RepeatMode} to be used for playback. + * + * @param repeatMode A repeat mode. + */ + void setRepeatMode(@RepeatMode int repeatMode); + + /** + * Returns the current {@link RepeatMode} used for playback. + * + * @return The current repeat mode. + */ + @RepeatMode int getRepeatMode(); + + /** + * Whether the player is currently loading the source. + * + * @return Whether the player is currently loading the source. + */ + boolean isLoading(); + + /** + * Seeks to the default position associated with the current window. The position can depend on + * the type of media being played. For live streams it will typically be the live edge of the + * window. For other streams it will typically be the start of the window. + */ + void seekToDefaultPosition(); + + /** + * Seeks to the default position associated with the specified window. The position can depend on + * the type of media being played. For live streams it will typically be the live edge of the + * window. For other streams it will typically be the start of the window. + * + * @param windowIndex The index of the window whose associated default position should be seeked + * to. + */ + void seekToDefaultPosition(int windowIndex); + + /** + * Seeks to a position specified in milliseconds in the current window. + * + * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to + * the window's default position. + */ + void seekTo(long positionMs); + + /** + * Seeks to a position specified in milliseconds in the specified window. + * + * @param windowIndex The index of the window. + * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to + * the window's default position. + */ + void seekTo(int windowIndex, long positionMs); + + /** + * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the + * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. + *

+ * Playback parameters changes may cause the player to buffer. + * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever + * the currently active playback parameters change. When that listener is called, the parameters + * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch + * may be out of range, in which case they are constrained to a set of permitted values. If it is + * not possible to change the playback parameters, the listener will not be invoked. + * + * @param playbackParameters The playback parameters, or {@code null} to use the defaults. + */ + void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters); + + /** + * Returns the currently active playback parameters. + * + * @see EventListener#onPlaybackParametersChanged(PlaybackParameters) + */ + PlaybackParameters getPlaybackParameters(); + + /** + * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention + * is to pause playback. + *

+ * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * player instance can still be used, and {@link #release()} must still be called on the player if + * it's no longer required. + *

+ * Calling this method does not reset the playback position. + */ + void stop(); + + /** + * Releases the player. This method must be called when the player is no longer required. The + * player must not be used after calling this method. + */ + void release(); + + /** + * Returns the number of renderers. + */ + int getRendererCount(); + + /** + * Returns the track type that the renderer at a given index handles. + * + * @see Renderer#getTrackType() + * @param index The index of the renderer. + * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. + */ + int getRendererType(int index); + + /** + * Returns the available track groups. + */ + TrackGroupArray getCurrentTrackGroups(); + + /** + * Returns the current track selections for each renderer. + */ + TrackSelectionArray getCurrentTrackSelections(); + + /** + * Returns the current manifest. The type depends on the type of media being played. May be null. + */ + @Nullable Object getCurrentManifest(); + + /** + * Returns the current {@link Timeline}. Never null, but may be empty. + */ + Timeline getCurrentTimeline(); + + /** + * Returns the index of the period currently being played. + */ + int getCurrentPeriodIndex(); + + /** + * Returns the index of the window currently being played. + */ + int getCurrentWindowIndex(); + + /** + * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the + * duration is not known. + */ + long getDuration(); + + /** + * Returns the playback position in the current window, in milliseconds. + */ + long getCurrentPosition(); + + /** + * Returns an estimate of the position in the current window up to which data is buffered, in + * milliseconds. + */ + long getBufferedPosition(); + + /** + * Returns an estimate of the percentage in the current window up to which data is buffered, or 0 + * if no estimate is available. + */ + int getBufferedPercentage(); + + /** + * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is + * empty. + * + * @see Timeline.Window#isDynamic + */ + boolean isCurrentWindowDynamic(); + + /** + * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is + * empty. + * + * @see Timeline.Window#isSeekable + */ + boolean isCurrentWindowSeekable(); + + /** + * Returns whether the player is currently playing an ad. + */ + boolean isPlayingAd(); + + /** + * If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period + * currently being played. Returns {@link C#INDEX_UNSET} otherwise. + */ + int getCurrentAdGroupIndex(); + + /** + * If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns + * {@link C#INDEX_UNSET} otherwise. + */ + int getCurrentAdIndexInAdGroup(); + + /** + * If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be + * played once all ads in the ad group have finished playing, in milliseconds. If there is no ad + * playing, the returned position is the same as that returned by {@link #getCurrentPosition()}. + */ + long getContentPosition(); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 151453c12c..f841a1b8b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -27,11 +27,11 @@ public interface RendererCapabilities { * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. */ - int FORMAT_SUPPORT_MASK = 0b11; + int FORMAT_SUPPORT_MASK = 0b111; /** * The {@link Renderer} is capable of rendering the format. */ - int FORMAT_HANDLED = 0b11; + int FORMAT_HANDLED = 0b100; /** * The {@link Renderer} is capable of rendering formats with the same mime type, but the * properties of the format exceed the renderer's capability. @@ -40,7 +40,16 @@ public interface RendererCapabilities { * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported * by the underlying H264 decoder. */ - int FORMAT_EXCEEDS_CAPABILITIES = 0b10; + int FORMAT_EXCEEDS_CAPABILITIES = 0b011; + /** + * The {@link Renderer} is capable of rendering formats with the same mime type, but the + * drm scheme used is not supported. + *

+ * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is + * {@link MimeTypes#VIDEO_H264}, but the format indicates cbcs encryption, which is not supported + * by the underlying content decryption module. + */ + int FORMAT_UNSUPPORTED_DRM = 0b010; /** * The {@link Renderer} is a general purpose renderer for formats of the same top-level type, * but is not capable of rendering the format or any other format with the same mime type because @@ -49,7 +58,7 @@ public interface RendererCapabilities { * Example: The {@link Renderer} is a general purpose audio renderer and the format's * mime type matches audio/[subtype], but there does not exist a suitable decoder for [subtype]. */ - int FORMAT_UNSUPPORTED_SUBTYPE = 0b01; + int FORMAT_UNSUPPORTED_SUBTYPE = 0b001; /** * The {@link Renderer} is not capable of rendering the format, either because it does not * support the format's top-level type, or because it's a specialized renderer for a different @@ -58,40 +67,40 @@ public interface RendererCapabilities { * Example: The {@link Renderer} is a general purpose video renderer, but the format has an * audio mime type. */ - int FORMAT_UNSUPPORTED_TYPE = 0b00; + int FORMAT_UNSUPPORTED_TYPE = 0b000; /** * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. */ - int ADAPTIVE_SUPPORT_MASK = 0b1100; + int ADAPTIVE_SUPPORT_MASK = 0b11000; /** * The {@link Renderer} can seamlessly adapt between formats. */ - int ADAPTIVE_SEAMLESS = 0b1000; + int ADAPTIVE_SEAMLESS = 0b10000; /** * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity * (~50-100ms) when adaptation occurs. */ - int ADAPTIVE_NOT_SEAMLESS = 0b0100; + int ADAPTIVE_NOT_SEAMLESS = 0b01000; /** * The {@link Renderer} does not support adaptation between formats. */ - int ADAPTIVE_NOT_SUPPORTED = 0b0000; + int ADAPTIVE_NOT_SUPPORTED = 0b00000; /** * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. */ - int TUNNELING_SUPPORT_MASK = 0b10000; + int TUNNELING_SUPPORT_MASK = 0b100000; /** * The {@link Renderer} supports tunneled output. */ - int TUNNELING_SUPPORTED = 0b10000; + int TUNNELING_SUPPORTED = 0b100000; /** * The {@link Renderer} does not support tunneled output. */ - int TUNNELING_NOT_SUPPORTED = 0b00000; + int TUNNELING_NOT_SUPPORTED = 0b000000; /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 6094513913..08e178878b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -27,6 +27,7 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; @@ -37,6 +38,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.List; @@ -105,8 +107,7 @@ public class SimpleExoPlayer implements ExoPlayer { private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; - @C.StreamType - private int audioStreamType; + private AudioAttributes audioAttributes; private float audioVolume; protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, @@ -136,7 +137,7 @@ public class SimpleExoPlayer implements ExoPlayer { // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; - audioStreamType = C.STREAM_TYPE_DEFAULT; + audioAttributes = AudioAttributes.DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; // Build the player and associated objects. @@ -221,8 +222,9 @@ public class SimpleExoPlayer implements ExoPlayer { if (surfaceHolder == null) { setVideoSurfaceInternal(null, false); } else { - setVideoSurfaceInternal(surfaceHolder.getSurface(), false); surfaceHolder.addCallback(componentListener); + Surface surface = surfaceHolder.getSurface(); + setVideoSurfaceInternal(surface != null && surface.isValid() ? surface : null, false); } } @@ -273,9 +275,10 @@ public class SimpleExoPlayer implements ExoPlayer { if (textureView.getSurfaceTextureListener() != null) { Log.w(TAG, "Replacing existing SurfaceTextureListener."); } - SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); - setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); textureView.setSurfaceTextureListener(componentListener); + SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture() + : null; + setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); } } @@ -292,33 +295,70 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Sets the stream type for audio playback (see {@link C.StreamType} and - * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type - * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}. + * Sets the stream type for audio playback, used by the underlying audio track. *

- * Note that when the stream type changes, the AudioTrack must be reinitialized, which can - * introduce a brief gap in audio output. Note also that tracks in the same audio session must - * share the same routing, so a new audio session id will be generated. + * Setting the stream type during playback may introduce a short gap in audio output as the audio + * track is recreated. A new audio session id will also be generated. + *

+ * Calling this method overwrites any attributes set previously by calling + * {@link #setAudioAttributes(AudioAttributes)}. * - * @param audioStreamType The stream type for audio playback. + * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}. + * @param streamType The stream type for audio playback. */ - public void setAudioStreamType(@C.StreamType int audioStreamType) { - this.audioStreamType = audioStreamType; + @Deprecated + public void setAudioStreamType(@C.StreamType int streamType) { + @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType); + @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build(); + setAudioAttributes(audioAttributes); + } + + /** + * Returns the stream type for audio playback. + * + * @deprecated Use {@link #getAudioAttributes()}. + */ + @Deprecated + public @C.StreamType int getAudioStreamType() { + return Util.getStreamTypeForAudioUsage(audioAttributes.usage); + } + + /** + * Sets the attributes for audio playback, used by the underlying audio track. If not set, the + * default audio attributes will be used. They are suitable for general media playback. + *

+ * Setting the audio attributes during playback may introduce a short gap in audio output as the + * audio track is recreated. A new audio session id will also be generated. + *

+ * If tunneling is enabled by the track selector, the specified audio attributes will be ignored, + * but they will take effect if audio is later played without tunneling. + *

+ * If the device is running a build before platform API version 21, audio attributes cannot be set + * directly on the underlying audio track. In this case, the usage will be mapped onto an + * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. + * + * @param audioAttributes The attributes to use for audio playback. + */ + public void setAudioAttributes(AudioAttributes audioAttributes) { + this.audioAttributes = audioAttributes; ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES, + audioAttributes); } } player.sendMessages(messages); } /** - * Returns the stream type for audio playback. + * Returns the attributes for audio playback. */ - public @C.StreamType int getAudioStreamType() { - return audioStreamType; + public AudioAttributes getAudioAttributes() { + return audioAttributes; } /** @@ -480,12 +520,17 @@ public class SimpleExoPlayer implements ExoPlayer { // ExoPlayer implementation @Override - public void addListener(EventListener listener) { + public Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + @Override + public void addListener(Player.EventListener listener) { player.addListener(listener); } @Override - public void removeListener(EventListener listener) { + public void removeListener(Player.EventListener listener) { player.removeListener(listener); } @@ -514,6 +559,16 @@ public class SimpleExoPlayer implements ExoPlayer { return player.getPlayWhenReady(); } + @Override + public @RepeatMode int getRepeatMode() { + return player.getRepeatMode(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + } + @Override public boolean isLoading() { return player.isLoading(); @@ -646,6 +701,26 @@ public class SimpleExoPlayer implements ExoPlayer { return player.isCurrentWindowSeekable(); } + @Override + public boolean isPlayingAd() { + return player.isPlayingAd(); + } + + @Override + public int getCurrentAdGroupIndex() { + return player.getCurrentAdGroupIndex(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return player.getCurrentAdIndexInAdGroup(); + } + + @Override + public long getContentPosition() { + return player.getContentPosition(); + } + // Internal methods. private void removeSurfaceCallbacks() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index eb3966ae4d..414c0804ad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -15,18 +15,24 @@ */ package com.google.android.exoplayer2; +import android.util.Pair; +import com.google.android.exoplayer2.util.Assertions; + /** - * A representation of media currently available for playback. - *

- * Timeline instances are immutable. For cases where the available media is changing dynamically - * (e.g. live streams) a timeline provides a snapshot of the media currently available. + * A flexible representation of the structure of media. A timeline is able to represent the + * structure of a wide variety of media, from simple cases like a single media file through to + * complex compositions of media such as playlists and streams with inserted ads. Instances are + * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides + * a snapshot of the current state. *

* A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single - * logical piece of media, for example a media file. A window spans one or more periods, defining - * the region within those periods that's currently available for playback along with additional - * information such as whether seeking is supported within the window. Each window defines a default - * position, which is the position from which playback will start when the player starts playing the - * window. The following examples illustrate timelines for various use cases. + * logical piece of media, for example a media file. It may also define groups of ads inserted into + * the media, along with information about whether those ads have been loaded and played. A window + * spans one or more periods, defining the region within those periods that's currently available + * for playback along with additional information such as whether seeking is supported within the + * window. Each window defines a default position, which is the position from which playback will + * start when the player starts playing the window. The following examples illustrate timelines for + * various use cases. * *

Single media file or on-demand stream

*

@@ -75,150 +81,36 @@ package com.google.android.exoplayer2; * with multiple periods"> *

* This case arises when a live stream is explicitly divided into separate periods, for example at - * content and advert boundaries. This case is similar to the Live stream - * with limited availability case, except that the window may span more than one period. - * Multiple periods are also possible in the indefinite availability case. + * content boundaries. This case is similar to the Live stream with limited + * availability case, except that the window may span more than one period. Multiple periods are + * also possible in the indefinite availability case. * - *

On-demand pre-roll followed by live stream

+ *

On-demand stream followed by live stream

*

- * Example timeline for an on-demand pre-roll
+ *   <img src= *

* This case is the concatenation of the Single media file or on-demand * stream and Live stream with multiple periods cases. When playback - * of the pre-roll ends, playback of the live stream will start from its default position near the - * live edge. + * of the on-demand stream ends, playback of the live stream will start from its default position + * near the live edge. + * + *

On-demand stream with mid-roll ads

+ *

+ * Example timeline for an on-demand
+ *       stream with mid-roll ad groups + *

+ * This case includes mid-roll ad groups, which are defined as part of the timeline's single period. + * The period can be queried for information about the ad groups and the ads they contain. */ public abstract class Timeline { - /** - * An empty timeline. - */ - public static final Timeline EMPTY = new Timeline() { - - @Override - public int getWindowCount() { - return 0; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - throw new IndexOutOfBoundsException(); - } - - @Override - public int getPeriodCount() { - return 0; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - throw new IndexOutOfBoundsException(); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return C.INDEX_UNSET; - } - - }; - - /** - * Returns whether the timeline is empty. - */ - public final boolean isEmpty() { - return getWindowCount() == 0; - } - - /** - * Returns the number of windows in the timeline. - */ - public abstract int getWindowCount(); - - /** - * Populates a {@link Window} with data for the window at the specified index. Does not populate - * {@link Window#id}. - * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @return The populated {@link Window}, for convenience. - */ - public final Window getWindow(int windowIndex, Window window) { - return getWindow(windowIndex, window, false); - } - - /** - * Populates a {@link Window} with data for the window at the specified index. - * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. - * @return The populated {@link Window}, for convenience. - */ - public Window getWindow(int windowIndex, Window window, boolean setIds) { - return getWindow(windowIndex, window, setIds, 0); - } - - /** - * Populates a {@link Window} with data for the window at the specified index. - * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to - * null. The caller should pass false for efficiency reasons unless the field is required. - * @param defaultPositionProjectionUs A duration into the future that the populated window's - * default start position should be projected. - * @return The populated {@link Window}, for convenience. - */ - public abstract Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs); - - /** - * Returns the number of periods in the timeline. - */ - public abstract int getPeriodCount(); - - /** - * Populates a {@link Period} with data for the period at the specified index. Does not populate - * {@link Period#id} and {@link Period#uid}. - * - * @param periodIndex The index of the period. - * @param period The {@link Period} to populate. Must not be null. - * @return The populated {@link Period}, for convenience. - */ - public final Period getPeriod(int periodIndex, Period period) { - return getPeriod(periodIndex, period, false); - } - - /** - * Populates a {@link Period} with data for the period at the specified index. - * - * @param periodIndex The index of the period. - * @param period The {@link Period} to populate. Must not be null. - * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false, - * the fields will be set to null. The caller should pass false for efficiency reasons unless - * the fields are required. - * @return The populated {@link Period}, for convenience. - */ - public abstract Period getPeriod(int periodIndex, Period period, boolean setIds); - - /** - * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET} - * if the period is not in the timeline. - * - * @param uid A unique identifier for a period. - * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found. - */ - public abstract int getIndexOfPeriod(Object uid); - /** * Holds information about a window in a {@link Timeline}. A window defines a region of media * currently available for playback along with additional information such as whether seeking is - * supported within the window. See {@link Timeline} for more details. The figure below shows some - * of the information defined by a window, as well as how this information relates to - * corresponding {@link Period}s in the timeline. + * supported within the window. The figure below shows some of the information defined by a + * window, as well as how this information relates to corresponding {@link Period}s in the + * timeline. *

* Information defined by a timeline window *

@@ -354,9 +246,11 @@ public abstract class Timeline { /** * Holds information about a period in a {@link Timeline}. A period defines a single logical piece - * of media, for example a a media file. See {@link Timeline} for more details. The figure below - * shows some of the information defined by a period, as well as how this information relates to a - * corresponding {@link Window} in the timeline. + * of media, for example a media file. It may also define groups of ads inserted into the media, + * along with information about whether those ads have been loaded and played. + *

+ * The figure below shows some of the information defined by a period, as well as how this + * information relates to a corresponding {@link Window} in the timeline. *

* Information defined by a period *

@@ -383,24 +277,71 @@ public abstract class Timeline { */ public long durationUs; - /** - * Whether this period contains an ad. - */ - public boolean isAd; - private long positionInWindowUs; + private long[] adGroupTimesUs; + private int[] adCounts; + private int[] adsLoadedCounts; + private int[] adsPlayedCounts; + private long[][] adDurationsUs; + private long adResumePositionUs; /** * Sets the data held by this period. + * + * @param id An identifier for the period. Not necessarily unique. + * @param uid A unique identifier for the period. + * @param windowIndex The index of the window to which this period belongs. + * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if + * unknown. + * @param positionInWindowUs The position of the start of this period relative to the start of + * the window to which it belongs, in milliseconds. May be negative if the start of the + * period is not within the window. + * @return This period, for convenience. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, boolean isAd) { + long positionInWindowUs) { + return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null, + null, C.TIME_UNSET); + } + + /** + * Sets the data held by this period. + * + * @param id An identifier for the period. Not necessarily unique. + * @param uid A unique identifier for the period. + * @param windowIndex The index of the window to which this period belongs. + * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if + * unknown. + * @param positionInWindowUs The position of the start of this period relative to the start of + * the window to which it belongs, in milliseconds. May be negative if the start of the + * period is not within the window. + * @param adGroupTimesUs The times of ad groups relative to the start of the period, in + * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that + * the period has a postroll ad. + * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} + * if the number of ads is not yet known. + * @param adsLoadedCounts The number of ads loaded so far in each ad group. + * @param adsPlayedCounts The number of ads played so far in each ad group. + * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element + * may be {@link C#TIME_UNSET} if the duration is not yet known. + * @param adResumePositionUs The position offset in the first unplayed ad at which to begin + * playback, in microseconds. + * @return This period, for convenience. + */ + public Period set(Object id, Object uid, int windowIndex, long durationUs, + long positionInWindowUs, long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, + int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; - this.isAd = isAd; + this.adGroupTimesUs = adGroupTimesUs; + this.adCounts = adCounts; + this.adsLoadedCounts = adsLoadedCounts; + this.adsPlayedCounts = adsPlayedCounts; + this.adDurationsUs = adDurationsUs; + this.adResumePositionUs = adResumePositionUs; return this; } @@ -436,6 +377,403 @@ public abstract class Timeline { return positionInWindowUs; } + /** + * Returns the number of ad groups in the period. + */ + public int getAdGroupCount() { + return adGroupTimesUs == null ? 0 : adGroupTimesUs.length; + } + + /** + * Returns the time of the ad group at index {@code adGroupIndex} in the period, in + * microseconds. + * + * @param adGroupIndex The ad group index. + * @return The time of the ad group at the index, in microseconds. + */ + public long getAdGroupTimeUs(int adGroupIndex) { + return adGroupTimesUs[adGroupIndex]; + } + + /** + * Returns the number of ads that have been played in the specified ad group in the period. + * + * @param adGroupIndex The ad group index. + * @return The number of ads that have been played. + */ + public int getPlayedAdCount(int adGroupIndex) { + return adsPlayedCounts[adGroupIndex]; + } + + /** + * Returns whether the ad group at index {@code adGroupIndex} has been played. + * + * @param adGroupIndex The ad group index. + * @return Whether the ad group at index {@code adGroupIndex} has been played. + */ + public boolean hasPlayedAdGroup(int adGroupIndex) { + return adCounts[adGroupIndex] != C.INDEX_UNSET + && adsPlayedCounts[adGroupIndex] == adCounts[adGroupIndex]; + } + + /** + * Returns the index of the ad group at or before {@code positionUs}, if that ad group is + * unplayed. Returns {@link C#INDEX_UNSET} if the ad group before {@code positionUs} has been + * played, or if there is no such ad group. + * + * @param positionUs The position at or before which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexForPositionUs(long positionUs) { + if (adGroupTimesUs == null) { + return C.INDEX_UNSET; + } + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = adGroupTimesUs.length - 1; + while (index >= 0 && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE + || adGroupTimesUs[index] > positionUs)) { + index--; + } + return index >= 0 && !hasPlayedAdGroup(index) ? index : C.INDEX_UNSET; + } + + /** + * Returns the index of the next unplayed ad group after {@code positionUs}. Returns + * {@link C#INDEX_UNSET} if there is no such ad group. + * + * @param positionUs The position after which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexAfterPositionUs(long positionUs) { + if (adGroupTimesUs == null) { + return C.INDEX_UNSET; + } + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = 0; + while (index < adGroupTimesUs.length && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE + && (positionUs >= adGroupTimesUs[index] || hasPlayedAdGroup(index))) { + index++; + } + return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; + } + + /** + * Returns the number of ads in the ad group at index {@code adGroupIndex}, or + * {@link C#LENGTH_UNSET} if not yet known. + * + * @param adGroupIndex The ad group index. + * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known. + */ + public int getAdCountInAdGroup(int adGroupIndex) { + return adCounts[adGroupIndex]; + } + + /** + * Returns whether the URL for the specified ad is known. + * + * @param adGroupIndex The ad group index. + * @param adIndexInAdGroup The ad index in the ad group. + * @return Whether the URL for the specified ad is known. + */ + public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) { + return adIndexInAdGroup < adsLoadedCounts[adGroupIndex]; + } + + /** + * Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at + * {@code adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known. + * + * @param adGroupIndex The ad group index. + * @param adIndexInAdGroup The ad index in the ad group. + * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known. + */ + public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) { + if (adIndexInAdGroup >= adDurationsUs[adGroupIndex].length) { + return C.TIME_UNSET; + } + return adDurationsUs[adGroupIndex][adIndexInAdGroup]; + } + + /** + * Returns the position offset in the first unplayed ad at which to begin playback, in + * microseconds. + */ + public long getAdResumePositionUs() { + return adResumePositionUs; + } + } + /** + * An empty timeline. + */ + public static final Timeline EMPTY = new Timeline() { + + @Override + public int getWindowCount() { + return 0; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + throw new IndexOutOfBoundsException(); + } + + @Override + public int getPeriodCount() { + return 0; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + throw new IndexOutOfBoundsException(); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return C.INDEX_UNSET; + } + + }; + + /** + * Returns whether the timeline is empty. + */ + public final boolean isEmpty() { + return getWindowCount() == 0; + } + + /** + * Returns the number of windows in the timeline. + */ + public abstract int getWindowCount(); + + /** + * Returns the index of the window after the window at index {@code windowIndex} depending on the + * {@code repeatMode}. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. + */ + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; + case Player.REPEAT_MODE_ONE: + return windowIndex; + case Player.REPEAT_MODE_ALL: + return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1; + default: + throw new IllegalStateException(); + } + } + + /** + * Returns the index of the window before the window at index {@code windowIndex} depending on the + * {@code repeatMode}. + * + * @param windowIndex Index of a window in the timeline. + * @param repeatMode A repeat mode. + * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. + */ + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; + case Player.REPEAT_MODE_ONE: + return windowIndex; + case Player.REPEAT_MODE_ALL: + return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1; + default: + throw new IllegalStateException(); + } + } + + /** + * Returns whether the given window is the last window of the timeline depending on the + * {@code repeatMode}. + * + * @param windowIndex A window index. + * @param repeatMode A repeat mode. + * @return Whether the window of the given index is the last window of the timeline. + */ + public final boolean isLastWindow(int windowIndex, @Player.RepeatMode int repeatMode) { + return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; + } + + /** + * Returns whether the given window is the first window of the timeline depending on the + * {@code repeatMode}. + * + * @param windowIndex A window index. + * @param repeatMode A repeat mode. + * @return Whether the window of the given index is the first window of the timeline. + */ + public final boolean isFirstWindow(int windowIndex, @Player.RepeatMode int repeatMode) { + return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; + } + + /** + * Populates a {@link Window} with data for the window at the specified index. Does not populate + * {@link Window#id}. + * + * @param windowIndex The index of the window. + * @param window The {@link Window} to populate. Must not be null. + * @return The populated {@link Window}, for convenience. + */ + public final Window getWindow(int windowIndex, Window window) { + return getWindow(windowIndex, window, false); + } + + /** + * Populates a {@link Window} with data for the window at the specified index. + * + * @param windowIndex The index of the window. + * @param window The {@link Window} to populate. Must not be null. + * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to + * null. The caller should pass false for efficiency reasons unless the field is required. + * @return The populated {@link Window}, for convenience. + */ + public Window getWindow(int windowIndex, Window window, boolean setIds) { + return getWindow(windowIndex, window, setIds, 0); + } + + /** + * Populates a {@link Window} with data for the window at the specified index. + * + * @param windowIndex The index of the window. + * @param window The {@link Window} to populate. Must not be null. + * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to + * null. The caller should pass false for efficiency reasons unless the field is required. + * @param defaultPositionProjectionUs A duration into the future that the populated window's + * default start position should be projected. + * @return The populated {@link Window}, for convenience. + */ + public abstract Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs); + + /** + * Returns the number of periods in the timeline. + */ + public abstract int getPeriodCount(); + + /** + * Returns the index of the period after the period at index {@code periodIndex} depending on the + * {@code repeatMode}. + * + * @param periodIndex Index of a period in the timeline. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. + */ + public final int getNextPeriodIndex(int periodIndex, Period period, Window window, + @Player.RepeatMode int repeatMode) { + int windowIndex = getPeriod(periodIndex, period).windowIndex; + if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { + int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode); + if (nextWindowIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return getWindow(nextWindowIndex, window).firstPeriodIndex; + } + return periodIndex + 1; + } + + /** + * Returns whether the given period is the last period of the timeline depending on the + * {@code repeatMode}. + * + * @param periodIndex A period index. + * @param period A {@link Period} to be used internally. Must not be null. + * @param window A {@link Window} to be used internally. Must not be null. + * @param repeatMode A repeat mode. + * @return Whether the period of the given index is the last period of the timeline. + */ + public final boolean isLastPeriod(int periodIndex, Period period, Window window, + @Player.RepeatMode int repeatMode) { + return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET; + } + + /** + * Populates a {@link Period} with data for the period at the specified index. Does not populate + * {@link Period#id} and {@link Period#uid}. + * + * @param periodIndex The index of the period. + * @param period The {@link Period} to populate. Must not be null. + * @return The populated {@link Period}, for convenience. + */ + public final Period getPeriod(int periodIndex, Period period) { + return getPeriod(periodIndex, period, false); + } + + /** + * Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position + * projection. + */ + public final Pair getPeriodPosition(Window window, Period period, int windowIndex, + long windowPositionUs) { + return getPeriodPosition(window, period, windowIndex, windowPositionUs, 0); + } + + /** + * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). + * + * @param window A {@link Window} that may be overwritten. + * @param period A {@link Period} that may be overwritten. + * @param windowIndex The window index. + * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default + * start position. + * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the + * duration into the future by which the window's position should be projected. + * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} + * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's + * position could not be projected by {@code defaultPositionProjectionUs}. + */ + public final Pair getPeriodPosition(Window window, Period period, int windowIndex, + long windowPositionUs, long defaultPositionProjectionUs) { + Assertions.checkIndex(windowIndex, 0, getWindowCount()); + getWindow(windowIndex, window, false, defaultPositionProjectionUs); + if (windowPositionUs == C.TIME_UNSET) { + windowPositionUs = window.getDefaultPositionUs(); + if (windowPositionUs == C.TIME_UNSET) { + return null; + } + } + int periodIndex = window.firstPeriodIndex; + long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; + long periodDurationUs = getPeriod(periodIndex, period).getDurationUs(); + while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs + && periodIndex < window.lastPeriodIndex) { + periodPositionUs -= periodDurationUs; + periodDurationUs = getPeriod(++periodIndex, period).getDurationUs(); + } + return Pair.create(periodIndex, periodPositionUs); + } + + /** + * Populates a {@link Period} with data for the period at the specified index. + * + * @param periodIndex The index of the period. + * @param period The {@link Period} to populate. Must not be null. + * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false, + * the fields will be set to null. The caller should pass false for efficiency reasons unless + * the fields are required. + * @return The populated {@link Period}, for convenience. + */ + public abstract Period getPeriod(int periodIndex, Period period, boolean setIds); + + /** + * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET} + * if the period is not in the timeline. + * + * @param uid A unique identifier for a period. + * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found. + */ + public abstract int getIndexOfPeriod(Object uid); + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java new file mode 100644 index 0000000000..337200da8f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import android.annotation.TargetApi; +import com.google.android.exoplayer2.C; + +/** + * Attributes for audio playback, which configure the underlying platform + * {@link android.media.AudioTrack}. + *

+ * To set the audio attributes, create an instance using the {@link Builder} and either pass it to + * {@link com.google.android.exoplayer2.SimpleExoPlayer#setAudioAttributes(AudioAttributes)} or + * send a message of type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to the audio renderers. + *

+ * This class is based on {@link android.media.AudioAttributes}, but can be used on all supported + * API versions. + */ +public final class AudioAttributes { + + public static final AudioAttributes DEFAULT = new Builder().build(); + + /** + * Builder for {@link AudioAttributes}. + */ + public static final class Builder { + + @C.AudioContentType + private int contentType; + @C.AudioFlags + private int flags; + @C.AudioUsage + private int usage; + + /** + * Creates a new builder for {@link AudioAttributes}. + *

+ * By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is + * {@link C#USAGE_MEDIA}, and no flags are set. + */ + public Builder() { + contentType = C.CONTENT_TYPE_UNKNOWN; + flags = 0; + usage = C.USAGE_MEDIA; + } + + /** + * @see android.media.AudioAttributes.Builder#setContentType(int) + */ + public Builder setContentType(@C.AudioContentType int contentType) { + this.contentType = contentType; + return this; + } + + /** + * @see android.media.AudioAttributes.Builder#setFlags(int) + */ + public Builder setFlags(@C.AudioFlags int flags) { + this.flags = flags; + return this; + } + + /** + * @see android.media.AudioAttributes.Builder#setUsage(int) + */ + public Builder setUsage(@C.AudioUsage int usage) { + this.usage = usage; + return this; + } + + /** + * Creates an {@link AudioAttributes} instance from this builder. + */ + public AudioAttributes build() { + return new AudioAttributes(contentType, flags, usage); + } + + } + + @C.AudioContentType + public final int contentType; + @C.AudioFlags + public final int flags; + @C.AudioUsage + public final int usage; + + private android.media.AudioAttributes audioAttributesV21; + + private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags, + @C.AudioUsage int usage) { + this.contentType = contentType; + this.flags = flags; + this.usage = usage; + } + + @TargetApi(21) + /* package */ android.media.AudioAttributes getAudioAttributesV21() { + if (audioAttributesV21 == null) { + audioAttributesV21 = new android.media.AudioAttributes.Builder() + .setContentType(contentType) + .setFlags(flags) + .setUsage(usage) + .build(); + } + return audioAttributesV21; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + AudioAttributes other = (AudioAttributes) obj; + return this.contentType == other.contentType && this.flags == other.flags + && this.usage == other.usage; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + contentType; + result = 31 * result + flags; + result = 31 * result + usage; + return result; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 92838e34b0..79cb26bf39 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.AudioAttributes; import android.media.AudioFormat; +import android.media.AudioManager; import android.media.AudioTimestamp; import android.os.ConditionVariable; import android.os.SystemClock; @@ -40,9 +40,9 @@ import java.util.LinkedList; *

* Before starting playback, specify the input format by calling * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)}, - * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()} - * to configure audio playback. These methods may be called after writing data to the track, in - * which case it will be reinitialized as required. + * {@link #setAudioAttributes(AudioAttributes)}, {@link #enableTunnelingV21(int)} and + * {@link #disableTunneling()} to configure audio playback. These methods may be called after + * writing data to the track, in which case it will be reinitialized as required. *

* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. @@ -299,8 +299,7 @@ public final class AudioTrack { private int encoding; @C.Encoding private int outputEncoding; - @C.StreamType - private int streamType; + private AudioAttributes audioAttributes; private boolean passthrough; private int bufferSize; private long bufferSizeUs; @@ -384,7 +383,7 @@ public final class AudioTrack { playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; - streamType = C.STREAM_TYPE_DEFAULT; + audioAttributes = AudioAttributes.DEFAULT; audioSessionId = C.AUDIO_SESSION_ID_UNSET; playbackParameters = PlaybackParameters.DEFAULT; drainingAudioProcessorIndex = C.INDEX_UNSET; @@ -634,19 +633,7 @@ public final class AudioTrack { // initialization of the audio track to fail. releasingConditionVariable.block(); - if (tunneling) { - audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, - bufferSize, audioSessionId); - } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM); - } else { - // Re-attach to the same audio session. - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM, audioSessionId); - } - checkAudioTrackInitialized(); - + audioTrack = initializeAudioTrack(); int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { @@ -657,12 +644,7 @@ public final class AudioTrack { releaseKeepSessionIdAudioTrack(); } if (keepSessionIdAudioTrack == null) { - int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. - int channelConfig = AudioFormat.CHANNEL_OUT_MONO; - @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; - int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. - keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, - channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId); + keepSessionIdAudioTrack = initializeKeepSessionIdAudioTrack(audioSessionId); } } } @@ -1021,23 +1003,23 @@ public final class AudioTrack { } /** - * Sets the stream type for audio track. If the stream type has changed and if the audio track + * Sets the attributes for audio playback. If the attributes have changed and if the audio track * is not configured for use with tunneling, then the audio track is reset and the audio session * id is cleared. *

- * If the audio track is configured for use with tunneling then the stream type is ignored, the - * audio track is not reset and the audio session id is not cleared. The passed stream type will - * be used if the audio track is later re-configured into non-tunneled mode. + * If the audio track is configured for use with tunneling then the audio attributes are ignored. + * The audio track is not reset and the audio session id is not cleared. The passed attributes + * will be used if the audio track is later re-configured into non-tunneled mode. * - * @param streamType The {@link C.StreamType} to use for audio output. + * @param audioAttributes The attributes for audio playback. */ - public void setStreamType(@C.StreamType int streamType) { - if (this.streamType == streamType) { + public void setAudioAttributes(AudioAttributes audioAttributes) { + if (this.audioAttributes.equals(audioAttributes)) { return; } - this.streamType = streamType; + this.audioAttributes = audioAttributes; if (tunneling) { - // The stream type is ignored in tunneling mode, so no need to reset. + // The audio attributes are ignored in tunneling mode, so no need to reset. return; } reset(); @@ -1333,31 +1315,6 @@ public final class AudioTrack { } } - /** - * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this - * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an - * exception is thrown. - * - * @throws InitializationException If {@link #audioTrack} has not been successfully initialized. - */ - private void checkAudioTrackInitialized() throws InitializationException { - int state = audioTrack.getState(); - if (state == STATE_INITIALIZED) { - return; - } - // The track is not successfully initialized. Release and null the track. - try { - audioTrack.release(); - } catch (Exception e) { - // The track has already failed to initialize, so it wouldn't be that surprising if release - // were to fail too. Swallow the exception. - } finally { - audioTrack = null; - } - - throw new InitializationException(state, sampleRate, channelConfig, bufferSize); - } - private boolean isInitialized() { return audioTrack != null; } @@ -1408,24 +1365,65 @@ public final class AudioTrack { && audioTrack.getPlaybackHeadPosition() == 0; } - /** - * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback. - */ + private android.media.AudioTrack initializeAudioTrack() throws InitializationException { + android.media.AudioTrack audioTrack; + if (Util.SDK_INT >= 21) { + audioTrack = createAudioTrackV21(); + } else { + int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage); + if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + outputEncoding, bufferSize, MODE_STREAM); + } else { + // Re-attach to the same audio session. + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + outputEncoding, bufferSize, MODE_STREAM, audioSessionId); + } + } + + int state = audioTrack.getState(); + if (state != STATE_INITIALIZED) { + try { + audioTrack.release(); + } catch (Exception e) { + // The track has already failed to initialize, so it wouldn't be that surprising if release + // were to fail too. Swallow the exception. + } + throw new InitializationException(state, sampleRate, channelConfig, bufferSize); + } + return audioTrack; + } + @TargetApi(21) - private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate, - int channelConfig, int encoding, int bufferSize, int sessionId) { - AudioAttributes attributesBuilder = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) - .setFlags(AudioAttributes.FLAG_HW_AV_SYNC) - .build(); + private android.media.AudioTrack createAudioTrackV21() { + android.media.AudioAttributes attributes; + if (tunneling) { + attributes = new android.media.AudioAttributes.Builder() + .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC) + .setUsage(android.media.AudioAttributes.USAGE_MEDIA) + .build(); + } else { + attributes = audioAttributes.getAudioAttributesV21(); + } AudioFormat format = new AudioFormat.Builder() .setChannelMask(channelConfig) - .setEncoding(encoding) + .setEncoding(outputEncoding) .setSampleRate(sampleRate) .build(); - return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM, - sessionId); + int audioSessionId = this.audioSessionId != C.AUDIO_SESSION_ID_UNSET ? this.audioSessionId + : AudioManager.AUDIO_SESSION_ID_GENERATE; + return new android.media.AudioTrack(attributes, format, bufferSize, MODE_STREAM, + audioSessionId); + } + + private android.media.AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { + int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. + int channelConfig = AudioFormat.CHANNEL_OUT_MONO; + @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; + int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. + return new android.media.AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding, + bufferSize, MODE_STATIC, audioSessionId); } @C.Encoding @@ -1465,7 +1463,7 @@ public final class AudioTrack { @TargetApi(21) private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack, ByteBuffer buffer, int size, long presentationTimeUs) { - // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed. + // TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed. // if (Util.SDK_INT >= 23) { // // The underlying platform AudioTrack writes AV sync headers directly. // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 48c7462b03..4d97c292ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -399,9 +399,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_STREAM_TYPE: - @C.StreamType int streamType = (Integer) message; - audioTrack.setStreamType(streamType); + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioTrack.setAudioAttributes(audioAttributes); break; default: super.handleMessage(messageType, message); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index ddb870f6ff..c4a55eeb02 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.Assertions; @@ -376,15 +377,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } @DrmSession.State int drmSessionState = drmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } private void processEndOfStream() throws ExoPlaybackException { @@ -514,13 +514,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements drmSession = pendingDrmSession; ExoMediaCrypto mediaCrypto = null; if (drmSession != null) { - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto(); - } else { + mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + } // The drm session isn't open yet. return; } @@ -595,9 +594,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements case C.MSG_SET_VOLUME: audioTrack.setVolume((Float) message); break; - case C.MSG_SET_STREAM_TYPE: - @C.StreamType int streamType = (Integer) message; - audioTrack.setStreamType(streamType); + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioTrack.setAudioAttributes(audioAttributes); break; default: super.handleMessage(messageType, message); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java index 0d143cdf49..ec17de8d74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -52,11 +52,11 @@ public final class CryptoInfo { /** * @see android.media.MediaCodec.CryptoInfo.Pattern */ - public int patternBlocksToEncrypt; + public int encryptedBlocks; /** * @see android.media.MediaCodec.CryptoInfo.Pattern */ - public int patternBlocksToSkip; + public int clearBlocks; private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; private final PatternHolderV24 patternHolder; @@ -70,28 +70,20 @@ public final class CryptoInfo { * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int) */ public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData, - byte[] key, byte[] iv, @C.CryptoMode int mode) { + byte[] key, byte[] iv, @C.CryptoMode int mode, int encryptedBlocks, int clearBlocks) { this.numSubSamples = numSubSamples; this.numBytesOfClearData = numBytesOfClearData; this.numBytesOfEncryptedData = numBytesOfEncryptedData; this.key = key; this.iv = iv; this.mode = mode; - patternBlocksToEncrypt = 0; - patternBlocksToSkip = 0; + this.encryptedBlocks = encryptedBlocks; + this.clearBlocks = clearBlocks; if (Util.SDK_INT >= 16) { updateFrameworkCryptoInfoV16(); } } - public void setPattern(int patternBlocksToEncrypt, int patternBlocksToSkip) { - this.patternBlocksToEncrypt = patternBlocksToEncrypt; - this.patternBlocksToSkip = patternBlocksToSkip; - if (Util.SDK_INT >= 24) { - patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); - } - } - /** * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. *

@@ -122,7 +114,7 @@ public final class CryptoInfo { frameworkCryptoInfo.iv = iv; frameworkCryptoInfo.mode = mode; if (Util.SDK_INT >= 24) { - patternHolder.set(patternBlocksToEncrypt, patternBlocksToSkip); + patternHolder.set(encryptedBlocks, clearBlocks); } } @@ -137,8 +129,8 @@ public final class CryptoInfo { pattern = new android.media.MediaCodec.CryptoInfo.Pattern(0, 0); } - private void set(int blocksToEncrypt, int blocksToSkip) { - pattern.set(blocksToEncrypt, blocksToSkip); + private void set(int encryptedBlocks, int clearBlocks) { + pattern.set(encryptedBlocks, clearBlocks); frameworkCryptoInfo.setPattern(pattern); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 6fc149ba32..cafbe6e8f7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -25,6 +25,7 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.support.annotation.IntDef; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -222,7 +223,6 @@ public class DefaultDrmSessionManager implements DrmSe this.eventHandler = eventHandler; this.eventListener = eventListener; mediaDrm.setOnEventListener(new MediaDrmEventListener()); - state = STATE_CLOSED; mode = MODE_PLAYBACK; } @@ -307,6 +307,26 @@ public class DefaultDrmSessionManager implements DrmSe // DrmSessionManager implementation. + @Override + public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { + SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null) { + // No data for this manager's scheme. + return false; + } + String schemeType = schemeData.type; + if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { + // If there is no scheme information, assume patternless AES-CTR. + return true; + } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType) + || C.CENC_TYPE_cens.equals(schemeType)) { + // AES-CBC and pattern encryption are supported on API 24 onwards. + return Util.SDK_INT >= 24; + } + // Unknown schemes, assume one of them is supported. + return true; + } + @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); @@ -358,7 +378,7 @@ public class DefaultDrmSessionManager implements DrmSe if (--openCount != 0) { return; } - state = STATE_CLOSED; + state = STATE_RELEASED; provisioningInProgress = false; mediaDrmHandler.removeCallbacksAndMessages(null); postResponseHandler.removeCallbacksAndMessages(null); @@ -384,35 +404,19 @@ public class DefaultDrmSessionManager implements DrmSe return state; } - @Override - public final T getMediaCrypto() { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - throw new IllegalStateException(); - } - return mediaCrypto; - } - - @Override - public boolean requiresSecureDecoderComponent(String mimeType) { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - throw new IllegalStateException(); - } - return mediaCrypto.requiresSecureDecoderComponent(mimeType); - } - @Override public final DrmSessionException getError() { return state == STATE_ERROR ? lastException : null; } + @Override + public final T getMediaCrypto() { + return mediaCrypto; + } + @Override public Map queryKeyStatus() { - // User may call this method rightfully even if state == STATE_ERROR. So only check if there is - // a sessionId - if (sessionId == null) { - throw new IllegalStateException(); - } - return mediaDrm.queryKeyStatus(sessionId); + return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); } @Override @@ -513,6 +517,8 @@ public class DefaultDrmSessionManager implements DrmSe } break; case MODE_RELEASE: + // It's not necessary to restore the key (and open a session to do that) before releasing it + // but this serves as a good sanity/fast-failure check. if (restoreKeys()) { postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 5126628dd9..9fa6547a00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; @@ -102,6 +103,33 @@ public final class DrmInitData implements Comparator, Parcelable { return schemeDatas[index]; } + /** + * Returns a copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated + * to have the specified scheme type. + * + * @param schemeType A protection scheme type. May be null. + * @return A copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated + * to have the specified scheme type. + */ + public DrmInitData copyWithSchemeType(@Nullable String schemeType) { + boolean isCopyRequired = false; + for (SchemeData schemeData : schemeDatas) { + if (!Util.areEqual(schemeData.type, schemeType)) { + isCopyRequired = true; + break; + } + } + if (isCopyRequired) { + SchemeData[] schemeDatas = new SchemeData[this.schemeDatas.length]; + for (int i = 0; i < schemeDatas.length; i++) { + schemeDatas[i] = this.schemeDatas[i].copyWithSchemeType(schemeType); + } + return new DrmInitData(schemeDatas); + } else { + return this; + } + } + @Override public int hashCode() { if (hashCode == 0) { @@ -167,6 +195,10 @@ public final class DrmInitData implements Comparator, Parcelable { * applies to all schemes). */ private final UUID uuid; + /** + * The protection scheme type, or null if not applicable or unknown. + */ + @Nullable public final String type; /** * The mimeType of {@link #data}. */ @@ -183,22 +215,26 @@ public final class DrmInitData implements Comparator, Parcelable { /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). + * @param type The type of the protection scheme, or null if not applicable or unknown. * @param mimeType The mimeType of the initialization data. * @param data The initialization data. */ - public SchemeData(UUID uuid, String mimeType, byte[] data) { - this(uuid, mimeType, data, false); + public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data) { + this(uuid, type, mimeType, data, false); } /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). + * @param type The type of the protection scheme, or null if not applicable or unknown. * @param mimeType The mimeType of the initialization data. * @param data The initialization data. * @param requiresSecureDecryption Whether secure decryption is required. */ - public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { + public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data, + boolean requiresSecureDecryption) { this.uuid = Assertions.checkNotNull(uuid); + this.type = type; this.mimeType = Assertions.checkNotNull(mimeType); this.data = Assertions.checkNotNull(data); this.requiresSecureDecryption = requiresSecureDecryption; @@ -206,6 +242,7 @@ public final class DrmInitData implements Comparator, Parcelable { /* package */ SchemeData(Parcel in) { uuid = new UUID(in.readLong(), in.readLong()); + type = in.readString(); mimeType = in.readString(); data = in.createByteArray(); requiresSecureDecryption = in.readByte() != 0; @@ -221,6 +258,19 @@ public final class DrmInitData implements Comparator, Parcelable { return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid); } + /** + * Returns a copy of the {@link SchemeData} instance with the given scheme type. + * + * @param type A protection scheme type. + * @return A copy of the {@link SchemeData} instance with the given scheme type. + */ + public SchemeData copyWithSchemeType(String type) { + if (Util.areEqual(this.type, type)) { + return this; + } + return new SchemeData(uuid, type, mimeType, data, requiresSecureDecryption); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof SchemeData)) { @@ -231,13 +281,14 @@ public final class DrmInitData implements Comparator, Parcelable { } SchemeData other = (SchemeData) obj; return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) - && Arrays.equals(data, other.data); + && Util.areEqual(type, other.type) && Arrays.equals(data, other.data); } @Override public int hashCode() { if (hashCode == 0) { int result = uuid.hashCode(); + result = 31 * result + (type == null ? 0 : type.hashCode()); result = 31 * result + mimeType.hashCode(); result = 31 * result + Arrays.hashCode(data); hashCode = result; @@ -256,6 +307,7 @@ public final class DrmInitData implements Comparator, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits()); + dest.writeString(type); dest.writeString(mimeType); dest.writeByteArray(data); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index cd694396b7..0c17b102fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -41,16 +41,16 @@ public interface DrmSession { * The state of the DRM session. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) - @interface State {} + @IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS}) + public @interface State {} + /** + * The session has been released. + */ + int STATE_RELEASED = 0; /** * The session has encountered an error. {@link #getError()} can be used to retrieve the cause. */ - int STATE_ERROR = 0; - /** - * The session is closed. - */ - int STATE_CLOSED = 1; + int STATE_ERROR = 1; /** * The session is being opened. */ @@ -65,66 +65,40 @@ public interface DrmSession { int STATE_OPENED_WITH_KEYS = 4; /** - * Returns the current state of the session. - * - * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, - * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. + * Returns the current state of the session, which is one of {@link #STATE_ERROR}, + * {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and + * {@link #STATE_OPENED_WITH_KEYS}. */ @State int getState(); - /** - * Returns a {@link ExoMediaCrypto} for the open session. - *

- * This method may be called when the session is in the following states: - * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} - * - * @return A {@link ExoMediaCrypto} for the open session. - * @throws IllegalStateException If called when a session isn't opened. - */ - T getMediaCrypto(); - - /** - * Whether the session requires a secure decoder for the specified mime type. - *

- * Normally this method should return - * {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases - * implementations may wish to modify the return value (i.e. to force a secure decoder even when - * one is not required). - *

- * This method may be called when the session is in the following states: - * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} - * - * @return Whether the open session requires a secure decoder for the specified mime type. - * @throws IllegalStateException If called when a session isn't opened. - */ - boolean requiresSecureDecoderComponent(String mimeType); - /** * Returns the cause of the error state. - *

- * This method may be called when the session is in any state. - * - * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. */ DrmSessionException getError(); /** - * Returns an informative description of the key status for the session. The status is in the form - * of {name, value} pairs. - * - *

Since DRM license policies vary by vendor, the specific status field names are determined by + * Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has + * been opened or after it's been released. + */ + T getMediaCrypto(); + + /** + * Returns a map describing the key status for the session, or null if called before the session + * has been opened or after it's been released. + *

+ * Since DRM license policies vary by vendor, the specific status field names are determined by * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names * for a particular DRM engine plugin. * - * @return A map of key status. - * @throws IllegalStateException If called when the session isn't opened. + * @return A map describing the key status for the session, or null if called before the session + * has been opened or after it's been released. * @see MediaDrm#queryKeyStatus(byte[]) */ Map queryKeyStatus(); /** - * Returns the key set id of the offline license loaded into this session, if there is one. Null - * otherwise. + * Returns the key set id of the offline license loaded into this session, or null if there isn't + * one. */ byte[] getOfflineLicenseKeySetId(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index 8e63fbfaae..e4b7059860 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -24,6 +24,16 @@ import android.os.Looper; @TargetApi(16) public interface DrmSessionManager { + /** + * Returns whether the manager is capable of acquiring a session for the given + * {@link DrmInitData}. + * + * @param drmInitData DRM initialization data. + * @return Whether the manager is capable of acquiring a session for the given + * {@link DrmInitData}. + */ + boolean canAcquireSession(DrmInitData drmInitData); + /** * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession} * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index dd441a022f..5bee85f449 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -26,9 +26,12 @@ import com.google.android.exoplayer2.util.Assertions; public final class FrameworkMediaCrypto implements ExoMediaCrypto { private final MediaCrypto mediaCrypto; + private final boolean forceAllowInsecureDecoderComponents; - /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) { + /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto, + boolean forceAllowInsecureDecoderComponents) { this.mediaCrypto = Assertions.checkNotNull(mediaCrypto); + this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; } public MediaCrypto getWrappedMediaCrypto() { @@ -37,7 +40,8 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto { @Override public boolean requiresSecureDecoderComponent(String mimeType) { - return mediaCrypto.requiresSecureDecoderComponent(mimeType); + return !forceAllowInsecureDecoderComponents + && mediaCrypto.requiresSecureDecoderComponent(mimeType); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index e6887af6da..ed4494559a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -24,7 +24,9 @@ import android.media.NotProvisionedException; import android.media.ResourceBusyException; import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -163,7 +165,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm PLAYREADY_KEY_REQUEST_PROPERTIES; - static { - PLAYREADY_KEY_REQUEST_PROPERTIES = new HashMap<>(); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("Content-Type", "text/xml"); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("SOAPAction", - "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); - } - private final HttpDataSource.Factory dataSourceFactory; private final String defaultUrl; private final Map keyRequestProperties; @@ -124,10 +116,15 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { url = defaultUrl; } Map requestProperties = new HashMap<>(); - requestProperties.put("Content-Type", "application/octet-stream"); + // Add standard request properties for supported schemes. + String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml" + : (C.CLEARKEY_UUID.equals(uuid) ? "application/json" : "application/octet-stream"); + requestProperties.put("Content-Type", contentType); if (C.PLAYREADY_UUID.equals(uuid)) { - requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); + requestProperties.put("SOAPAction", + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); } + // Add additional request properties. synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java index e5d014f102..45c38b3609 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java @@ -34,14 +34,16 @@ public final class WidevineUtil { /** * Returns license and playback durations remaining in seconds. * - * @return A {@link Pair} consisting of the remaining license and playback durations in seconds. - * @throws IllegalStateException If called when a session isn't opened. - * @param drmSession + * @param drmSession The drm session to query. + * @return A {@link Pair} consisting of the remaining license and playback durations in seconds, + * or null if called before the session has been opened or after it's been released. */ public static Pair getLicenseDurationRemainingSec(DrmSession drmSession) { Map keyStatus = drmSession.queryKeyStatus(); - return new Pair<>( - getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), + if (keyStatus == null) { + return null; + } + return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index c47a91b176..ccc5c0eb3e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -117,7 +117,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { /** * Sets the mode for {@link TsExtractor} instances created by the factory. * - * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory). + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory) * @param mode The mode to use. * @return The factory, for convenience. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java deleted file mode 100644 index 1c9a148226..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ /dev/null @@ -1,997 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.extractor; - -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.upstream.Allocation; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; -import java.io.EOFException; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from - * that queue. - */ -public final class DefaultTrackOutput implements TrackOutput { - - /** - * A listener for changes to the upstream format. - */ - public interface UpstreamFormatChangedListener { - - /** - * Called on the loading thread when an upstream format change occurs. - * - * @param format The new upstream format. - */ - void onUpstreamFormatChanged(Format format); - - } - - private static final int INITIAL_SCRATCH_SIZE = 32; - - private static final int STATE_ENABLED = 0; - private static final int STATE_ENABLED_WRITING = 1; - private static final int STATE_DISABLED = 2; - - private final Allocator allocator; - private final int allocationLength; - - private final InfoQueue infoQueue; - private final LinkedBlockingDeque dataQueue; - private final BufferExtrasHolder extrasHolder; - private final ParsableByteArray scratch; - private final AtomicInteger state; - - // Accessed only by the consuming thread. - private long totalBytesDropped; - private Format downstreamFormat; - - // Accessed only by the loading thread (or the consuming thread when there is no loading thread). - private boolean pendingFormatAdjustment; - private Format lastUnadjustedFormat; - private long sampleOffsetUs; - private long totalBytesWritten; - private Allocation lastAllocation; - private int lastAllocationOffset; - private boolean pendingSplice; - private UpstreamFormatChangedListener upstreamFormatChangeListener; - - /** - * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. - */ - public DefaultTrackOutput(Allocator allocator) { - this.allocator = allocator; - allocationLength = allocator.getIndividualAllocationLength(); - infoQueue = new InfoQueue(); - dataQueue = new LinkedBlockingDeque<>(); - extrasHolder = new BufferExtrasHolder(); - scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); - state = new AtomicInteger(); - lastAllocationOffset = allocationLength; - } - - // Called by the consuming thread, but only when there is no loading thread. - - /** - * Resets the output. - * - * @param enable Whether the output should be enabled. False if it should be disabled. - */ - public void reset(boolean enable) { - int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED); - clearSampleData(); - infoQueue.resetLargestParsedTimestamps(); - if (previousState == STATE_DISABLED) { - downstreamFormat = null; - } - } - - /** - * Sets a source identifier for subsequent samples. - * - * @param sourceId The source identifier. - */ - public void sourceId(int sourceId) { - infoQueue.sourceId(sourceId); - } - - /** - * Indicates that samples subsequently queued to the buffer should be spliced into those already - * queued. - */ - public void splice() { - pendingSplice = true; - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return infoQueue.getWriteIndex(); - } - - /** - * Discards samples from the write side of the buffer. - * - * @param discardFromIndex The absolute index of the first sample to be discarded. - */ - public void discardUpstreamSamples(int discardFromIndex) { - totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex); - dropUpstreamFrom(totalBytesWritten); - } - - /** - * Discards data from the write side of the buffer. Data is discarded from the specified absolute - * position. Any allocations that are fully discarded are returned to the allocator. - * - * @param absolutePosition The absolute position (inclusive) from which to discard data. - */ - private void dropUpstreamFrom(long absolutePosition) { - int relativePosition = (int) (absolutePosition - totalBytesDropped); - // Calculate the index of the allocation containing the position, and the offset within it. - int allocationIndex = relativePosition / allocationLength; - int allocationOffset = relativePosition % allocationLength; - // We want to discard any allocations after the one at allocationIdnex. - int allocationDiscardCount = dataQueue.size() - allocationIndex - 1; - if (allocationOffset == 0) { - // If the allocation at allocationIndex is empty, we should discard that one too. - allocationDiscardCount++; - } - // Discard the allocations. - for (int i = 0; i < allocationDiscardCount; i++) { - allocator.release(dataQueue.removeLast()); - } - // Update lastAllocation and lastAllocationOffset to reflect the new position. - lastAllocation = dataQueue.peekLast(); - lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset; - } - - // Called by the consuming thread. - - /** - * Disables buffering of sample data and metadata. - */ - public void disable() { - if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) { - clearSampleData(); - } - } - - /** - * Returns whether the buffer is empty. - */ - public boolean isEmpty() { - return infoQueue.isEmpty(); - } - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return infoQueue.getReadIndex(); - } - - /** - * Peeks the source id of the next sample, or the current upstream source id if the buffer is - * empty. - * - * @return The source id. - */ - public int peekSourceId() { - return infoQueue.peekSourceId(); - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public Format getUpstreamFormat() { - return infoQueue.getUpstreamFormat(); - } - - /** - * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *

- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public long getLargestQueuedTimestampUs() { - return infoQueue.getLargestQueuedTimestampUs(); - } - - /** - * Skips all samples currently in the buffer. - */ - public void skipAll() { - long nextOffset = infoQueue.skipAll(); - if (nextOffset != C.POSITION_UNSET) { - dropDownstreamTo(nextOffset); - } - } - - /** - * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer - * contains a keyframe with a timestamp of {@code timeUs} or earlier. If - * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} - * falls within the buffer. - * - * @param timeUs The seek time. - * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end - * of the buffer. - * @return Whether the skip was successful. - */ - public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer); - if (nextOffset == C.POSITION_UNSET) { - return false; - } - dropDownstreamTo(nextOffset); - return true; - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. - * @param formatRequired Whether the caller requires that the format of the stream be read even if - * it's not changing. A sample will never be read if set to true, however it is still possible - * for the end of stream or nothing to be read. - * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will - * be set if the buffer's timestamp is less than this value. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or - * {@link C#RESULT_BUFFER_READ}. - */ - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, - boolean loadingFinished, long decodeOnlyUntilUs) { - int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, - downstreamFormat, extrasHolder); - switch (result) { - case C.RESULT_FORMAT_READ: - downstreamFormat = formatHolder.format; - return C.RESULT_FORMAT_READ; - case C.RESULT_BUFFER_READ: - if (!buffer.isEndOfStream()) { - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); - } - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); - } - // Write the sample data into the holder. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); - // Advance the read head. - dropDownstreamTo(extrasHolder.nextOffset); - } - return C.RESULT_BUFFER_READ; - case C.RESULT_NOTHING_READ: - return C.RESULT_NOTHING_READ; - default: - throw new IllegalStateException(); - } - } - - /** - * Reads encryption data for the current sample. - *

- * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and - * {@link BufferExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The - * same value is added to {@link BufferExtrasHolder#offset}. - * - * @param buffer The buffer into which the encryption data should be written. - * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. - */ - private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) { - long offset = extrasHolder.offset; - - // Read the signal byte. - scratch.reset(1); - readData(offset, scratch.data, 1); - offset++; - byte signalByte = scratch.data[0]; - boolean subsampleEncryption = (signalByte & 0x80) != 0; - int ivSize = signalByte & 0x7F; - - // Read the initialization vector. - if (buffer.cryptoInfo.iv == null) { - buffer.cryptoInfo.iv = new byte[16]; - } - readData(offset, buffer.cryptoInfo.iv, ivSize); - offset += ivSize; - - // Read the subsample count, if present. - int subsampleCount; - if (subsampleEncryption) { - scratch.reset(2); - readData(offset, scratch.data, 2); - offset += 2; - subsampleCount = scratch.readUnsignedShort(); - } else { - subsampleCount = 1; - } - - // Write the clear and encrypted subsample sizes. - int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; - if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { - clearDataSizes = new int[subsampleCount]; - } - int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; - if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { - encryptedDataSizes = new int[subsampleCount]; - } - if (subsampleEncryption) { - int subsampleDataLength = 6 * subsampleCount; - scratch.reset(subsampleDataLength); - readData(offset, scratch.data, subsampleDataLength); - offset += subsampleDataLength; - scratch.setPosition(0); - for (int i = 0; i < subsampleCount; i++) { - clearDataSizes[i] = scratch.readUnsignedShort(); - encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); - } - } else { - clearDataSizes[0] = 0; - encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); - } - - // Populate the cryptoInfo. - buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, - extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR); - - // Adjust the offset and size to take into account the bytes read. - int bytesRead = (int) (offset - extrasHolder.offset); - extrasHolder.offset += bytesRead; - extrasHolder.size -= bytesRead; - } - - /** - * Reads data from the front of the rolling buffer. - * - * @param absolutePosition The absolute position from which data should be read. - * @param target The buffer into which data should be written. - * @param length The number of bytes to read. - */ - private void readData(long absolutePosition, ByteBuffer target, int length) { - int remaining = length; - while (remaining > 0) { - dropDownstreamTo(absolutePosition); - int positionInAllocation = (int) (absolutePosition - totalBytesDropped); - int toCopy = Math.min(remaining, allocationLength - positionInAllocation); - Allocation allocation = dataQueue.peek(); - target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy); - absolutePosition += toCopy; - remaining -= toCopy; - } - } - - /** - * Reads data from the front of the rolling buffer. - * - * @param absolutePosition The absolute position from which data should be read. - * @param target The array into which data should be written. - * @param length The number of bytes to read. - */ - private void readData(long absolutePosition, byte[] target, int length) { - int bytesRead = 0; - while (bytesRead < length) { - dropDownstreamTo(absolutePosition); - int positionInAllocation = (int) (absolutePosition - totalBytesDropped); - int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation); - Allocation allocation = dataQueue.peek(); - System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target, - bytesRead, toCopy); - absolutePosition += toCopy; - bytesRead += toCopy; - } - } - - /** - * Discard any allocations that hold data prior to the specified absolute position, returning - * them to the allocator. - * - * @param absolutePosition The absolute position up to which allocations can be discarded. - */ - private void dropDownstreamTo(long absolutePosition) { - int relativePosition = (int) (absolutePosition - totalBytesDropped); - int allocationIndex = relativePosition / allocationLength; - for (int i = 0; i < allocationIndex; i++) { - allocator.release(dataQueue.remove()); - totalBytesDropped += allocationLength; - } - } - - // Called by the loading thread. - - /** - * Sets a listener to be notified of changes to the upstream format. - * - * @param listener The listener. - */ - public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { - upstreamFormatChangeListener = listener; - } - - /** - * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * subsequently queued to the buffer. - * - * @param sampleOffsetUs The timestamp offset in microseconds. - */ - public void setSampleOffsetUs(long sampleOffsetUs) { - if (this.sampleOffsetUs != sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - pendingFormatAdjustment = true; - } - } - - @Override - public void format(Format format) { - Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); - boolean formatChanged = infoQueue.format(adjustedFormat); - lastUnadjustedFormat = format; - pendingFormatAdjustment = false; - if (upstreamFormatChangeListener != null && formatChanged) { - upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); - } - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - if (!startWriteOperation()) { - int bytesSkipped = input.skip(length); - if (bytesSkipped == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); - } - return bytesSkipped; - } - try { - length = prepareForAppend(length); - int bytesAppended = input.read(lastAllocation.data, - lastAllocation.translateOffset(lastAllocationOffset), length); - if (bytesAppended == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); - } - lastAllocationOffset += bytesAppended; - totalBytesWritten += bytesAppended; - return bytesAppended; - } finally { - endWriteOperation(); - } - } - - @Override - public void sampleData(ParsableByteArray buffer, int length) { - if (!startWriteOperation()) { - buffer.skipBytes(length); - return; - } - while (length > 0) { - int thisAppendLength = prepareForAppend(length); - buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset), - thisAppendLength); - lastAllocationOffset += thisAppendLength; - totalBytesWritten += thisAppendLength; - length -= thisAppendLength; - } - endWriteOperation(); - } - - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - if (pendingFormatAdjustment) { - format(lastUnadjustedFormat); - } - if (!startWriteOperation()) { - infoQueue.commitSampleTimestamp(timeUs); - return; - } - try { - if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) { - return; - } - pendingSplice = false; - } - timeUs += sampleOffsetUs; - long absoluteOffset = totalBytesWritten - size - offset; - infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); - } finally { - endWriteOperation(); - } - } - - // Private methods. - - private boolean startWriteOperation() { - return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING); - } - - private void endWriteOperation() { - if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) { - clearSampleData(); - } - } - - private void clearSampleData() { - infoQueue.clearSampleData(); - allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()])); - dataQueue.clear(); - allocator.trim(); - totalBytesDropped = 0; - totalBytesWritten = 0; - lastAllocation = null; - lastAllocationOffset = allocationLength; - } - - /** - * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the - * number of bytes that can actually be appended. - */ - private int prepareForAppend(int length) { - if (lastAllocationOffset == allocationLength) { - lastAllocationOffset = 0; - lastAllocation = allocator.allocate(); - dataQueue.add(lastAllocation); - } - return Math.min(length, allocationLength - lastAllocationOffset); - } - - /** - * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}. - * - * @param format The {@link Format} to adjust. - * @param sampleOffsetUs The offset to apply. - * @return The adjusted {@link Format}. - */ - private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) { - if (format == null) { - return null; - } - if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { - format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs); - } - return format; - } - - /** - * Holds information about the samples in the rolling buffer. - */ - private static final class InfoQueue { - - private static final int SAMPLE_CAPACITY_INCREMENT = 1000; - - private int capacity; - - private int[] sourceIds; - private long[] offsets; - private int[] sizes; - private int[] flags; - private long[] timesUs; - private byte[][] encryptionKeys; - private Format[] formats; - - private int queueSize; - private int absoluteReadIndex; - private int relativeReadIndex; - private int relativeWriteIndex; - - private long largestDequeuedTimestampUs; - private long largestQueuedTimestampUs; - private boolean upstreamKeyframeRequired; - private boolean upstreamFormatRequired; - private Format upstreamFormat; - private int upstreamSourceId; - - public InfoQueue() { - capacity = SAMPLE_CAPACITY_INCREMENT; - sourceIds = new int[capacity]; - offsets = new long[capacity]; - timesUs = new long[capacity]; - flags = new int[capacity]; - sizes = new int[capacity]; - encryptionKeys = new byte[capacity][]; - formats = new Format[capacity]; - largestDequeuedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - upstreamFormatRequired = true; - upstreamKeyframeRequired = true; - } - - public void clearSampleData() { - absoluteReadIndex = 0; - relativeReadIndex = 0; - relativeWriteIndex = 0; - queueSize = 0; - upstreamKeyframeRequired = true; - } - - // Called by the consuming thread, but only when there is no loading thread. - - public void resetLargestParsedTimestamps() { - largestDequeuedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return absoluteReadIndex + queueSize; - } - - /** - * Discards samples from the write side of the buffer. - * - * @param discardFromIndex The absolute index of the first sample to be discarded. - * @return The reduced total number of bytes written, after the samples have been discarded. - */ - public long discardUpstreamSamples(int discardFromIndex) { - int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize); - - if (discardCount == 0) { - if (absoluteReadIndex == 0) { - // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue. - return 0; - } - int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; - return offsets[lastWriteIndex] + sizes[lastWriteIndex]; - } - - queueSize -= discardCount; - relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity; - // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are - // always less than the timestamp of the keyframe itself, and of subsequent frames. - largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = queueSize - 1; i >= 0; i--) { - int sampleIndex = (relativeReadIndex + i) % capacity; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]); - if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - break; - } - } - return offsets[relativeWriteIndex]; - } - - public void sourceId(int sourceId) { - upstreamSourceId = sourceId; - } - - // Called by the consuming thread. - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return absoluteReadIndex; - } - - /** - * Peeks the source id of the next sample, or the current upstream source id if the queue is - * empty. - */ - public int peekSourceId() { - return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex]; - } - - /** - * Returns whether the queue is empty. - */ - public synchronized boolean isEmpty() { - return queueSize == 0; - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public synchronized Format getUpstreamFormat() { - return upstreamFormatRequired ? null : upstreamFormat; - } - - /** - * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *

- * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public synchronized long getLargestQueuedTimestampUs() { - return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs); - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If a sample is read then the buffer is populated with information - * about the sample, but not its data. The size and absolute position of the data in the - * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present - * and the absolute position of the first byte that may still be required after the current - * sample has been read. May be null if the caller requires that the format of the stream be - * read even if it's not changing. - * @param formatRequired Whether the caller requires that the format of the stream be read even - * if it's not changing. A sample will never be read if set to true, however it is still - * possible for the end of stream or nothing to be read. - * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param downstreamFormat The current downstream {@link Format}. If the format of the next - * sample is different to the current downstream format then a format will be read. - * @param extrasHolder The holder into which extra sample information should be written. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} - * or {@link C#RESULT_BUFFER_READ}. - */ - @SuppressWarnings("ReferenceEquality") - public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired, boolean loadingFinished, Format downstreamFormat, - BufferExtrasHolder extrasHolder) { - if (queueSize == 0) { - if (loadingFinished) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null - && (formatRequired || upstreamFormat != downstreamFormat)) { - formatHolder.format = upstreamFormat; - return C.RESULT_FORMAT_READ; - } else { - return C.RESULT_NOTHING_READ; - } - } - - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - formatHolder.format = formats[relativeReadIndex]; - return C.RESULT_FORMAT_READ; - } - - if (buffer.isFlagsOnly()) { - return C.RESULT_NOTHING_READ; - } - - buffer.timeUs = timesUs[relativeReadIndex]; - buffer.setFlags(flags[relativeReadIndex]); - extrasHolder.size = sizes[relativeReadIndex]; - extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex]; - - largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs); - queueSize--; - relativeReadIndex++; - absoluteReadIndex++; - if (relativeReadIndex == capacity) { - // Wrap around. - relativeReadIndex = 0; - } - - extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex] - : extrasHolder.offset + extrasHolder.size; - return C.RESULT_BUFFER_READ; - } - - /** - * Skips all samples in the buffer. - * - * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no - * dropping of data is required. - */ - public synchronized long skipAll() { - if (queueSize == 0) { - return C.POSITION_UNSET; - } - - int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; - relativeReadIndex = (relativeReadIndex + queueSize) % capacity; - absoluteReadIndex += queueSize; - queueSize = 0; - return offsets[lastSampleIndex] + sizes[lastSampleIndex]; - } - - /** - * Attempts to locate the keyframe before or at the specified time. If - * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} - * falls within the buffer. - * - * @param timeUs The seek time. - * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end - * of the buffer. - * @return The offset of the keyframe's data if the keyframe was present. - * {@link C#POSITION_UNSET} otherwise. - */ - public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) { - if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { - return C.POSITION_UNSET; - } - - if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) { - return C.POSITION_UNSET; - } - - // This could be optimized to use a binary search, however in practice callers to this method - // often pass times near to the start of the buffer. Hence it's unclear whether switching to - // a binary search would yield any real benefit. - int sampleCount = 0; - int sampleCountToKeyframe = -1; - int searchIndex = relativeReadIndex; - while (searchIndex != relativeWriteIndex) { - if (timesUs[searchIndex] > timeUs) { - // We've gone too far. - break; - } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - // We've found a keyframe, and we're still before the seek position. - sampleCountToKeyframe = sampleCount; - } - searchIndex = (searchIndex + 1) % capacity; - sampleCount++; - } - - if (sampleCountToKeyframe == -1) { - return C.POSITION_UNSET; - } - - relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; - absoluteReadIndex += sampleCountToKeyframe; - queueSize -= sampleCountToKeyframe; - return offsets[relativeReadIndex]; - } - - // Called by the loading thread. - - public synchronized boolean format(Format format) { - if (format == null) { - upstreamFormatRequired = true; - return false; - } - upstreamFormatRequired = false; - if (Util.areEqual(format, upstreamFormat)) { - // Suppress changes between equal formats so we can use referential equality in readData. - return false; - } else { - upstreamFormat = format; - return true; - } - } - - public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, - int size, byte[] encryptionKey) { - if (upstreamKeyframeRequired) { - if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - upstreamKeyframeRequired = false; - } - Assertions.checkState(!upstreamFormatRequired); - commitSampleTimestamp(timeUs); - timesUs[relativeWriteIndex] = timeUs; - offsets[relativeWriteIndex] = offset; - sizes[relativeWriteIndex] = size; - flags[relativeWriteIndex] = sampleFlags; - encryptionKeys[relativeWriteIndex] = encryptionKey; - formats[relativeWriteIndex] = upstreamFormat; - sourceIds[relativeWriteIndex] = upstreamSourceId; - // Increment the write index. - queueSize++; - if (queueSize == capacity) { - // Increase the capacity. - int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; - int[] newSourceIds = new int[newCapacity]; - long[] newOffsets = new long[newCapacity]; - long[] newTimesUs = new long[newCapacity]; - int[] newFlags = new int[newCapacity]; - int[] newSizes = new int[newCapacity]; - byte[][] newEncryptionKeys = new byte[newCapacity][]; - Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeReadIndex; - System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); - System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap); - System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeReadIndex; - System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); - System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); - System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); - System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); - System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap); - System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); - System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); - offsets = newOffsets; - timesUs = newTimesUs; - flags = newFlags; - sizes = newSizes; - encryptionKeys = newEncryptionKeys; - formats = newFormats; - sourceIds = newSourceIds; - relativeReadIndex = 0; - relativeWriteIndex = capacity; - queueSize = capacity; - capacity = newCapacity; - } else { - relativeWriteIndex++; - if (relativeWriteIndex == capacity) { - // Wrap around. - relativeWriteIndex = 0; - } - } - } - - public synchronized void commitSampleTimestamp(long timeUs) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - } - - /** - * Attempts to discard samples from the tail of the queue to allow samples starting from the - * specified timestamp to be spliced in. - * - * @param timeUs The timestamp at which the splice occurs. - * @return Whether the splice was successful. - */ - public synchronized boolean attemptSplice(long timeUs) { - if (largestDequeuedTimestampUs >= timeUs) { - return false; - } - int retainCount = queueSize; - while (retainCount > 0 - && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) { - retainCount--; - } - discardUpstreamSamples(absoluteReadIndex + retainCount); - return true; - } - - } - - /** - * Holds additional buffer information not held by {@link DecoderInputBuffer}. - */ - private static final class BufferExtrasHolder { - - public int size; - public long offset; - public long nextOffset; - public byte[] encryptionKeyId; - - } - -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java index 61f97887be..c023b0de95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java @@ -51,7 +51,7 @@ public final class DummyTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { + CryptoData cryptoData) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index de3dfd5266..7a2bc15da9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -63,7 +63,8 @@ public interface Extractor { void init(ExtractorOutput output); /** - * Extracts data read from a provided {@link ExtractorInput}. + * Extracts data read from a provided {@link ExtractorInput}. Must not be called before + * {@link #init(ExtractorOutput)}. *

* A single call to this method will block until some progress has been made, but will not block * for longer than this. Hence each call will consume only a small amount of input data. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java index c4dee4b6a7..a12a0315a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java @@ -20,12 +20,78 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; +import java.util.Arrays; /** * Receives track level data extracted by an {@link Extractor}. */ public interface TrackOutput { + /** + * Holds data required to decrypt a sample. + */ + final class CryptoData { + + /** + * The encryption mode used for the sample. + */ + @C.CryptoMode public final int cryptoMode; + + /** + * The encryption key associated with the sample. Its contents must not be modified. + */ + public final byte[] encryptionKey; + + /** + * The number of encrypted blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int encryptedBlocks; + + /** + * The number of clear blocks in the encryption pattern, 0 if pattern encryption does not + * apply. + */ + public final int clearBlocks; + + /** + * @param cryptoMode See {@link #cryptoMode}. + * @param encryptionKey See {@link #encryptionKey}. + * @param encryptedBlocks See {@link #encryptedBlocks}. + * @param clearBlocks See {@link #clearBlocks}. + */ + public CryptoData(@C.CryptoMode int cryptoMode, byte[] encryptionKey, int encryptedBlocks, + int clearBlocks) { + this.cryptoMode = cryptoMode; + this.encryptionKey = encryptionKey; + this.encryptedBlocks = encryptedBlocks; + this.clearBlocks = clearBlocks; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CryptoData other = (CryptoData) obj; + return cryptoMode == other.cryptoMode && encryptedBlocks == other.encryptedBlocks + && clearBlocks == other.clearBlocks && Arrays.equals(encryptionKey, other.encryptionKey); + } + + @Override + public int hashCode() { + int result = cryptoMode; + result = 31 * result + Arrays.hashCode(encryptionKey); + result = 31 * result + encryptedBlocks; + result = 31 * result + clearBlocks; + return result; + } + + } + /** * Called when the {@link Format} of the track has been extracted from the stream. * @@ -70,9 +136,9 @@ public interface TrackOutput { * {@link #sampleData(ExtractorInput, int, boolean)} or * {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample * whose metadata is being passed. - * @param encryptionKey The encryption key associated with the sample. May be null. + * @param encryptionData The encryption data required to decrypt the sample. May be null. */ void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey); + CryptoData encryptionData); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 2313392fcf..6c4eb033ce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -120,6 +120,7 @@ public final class MatroskaExtractor implements Extractor { private static final String CODEC_ID_ACM = "A_MS/ACM"; private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT"; private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; + private static final String CODEC_ID_ASS = "S_TEXT/ASS"; private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; private static final String CODEC_ID_PGS = "S_HDMV/PGS"; private static final String CODEC_ID_DVBSUB = "S_DVBSUB"; @@ -226,21 +227,62 @@ public final class MatroskaExtractor implements Extractor { private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10}; /** - * A special end timecode indicating that a subtitle should be displayed until the next subtitle, - * or until the end of the media in the case of the last subtitle. + * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. + */ + private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19; + /** + * A special end timecode indicating that a subrip subtitle should be displayed until the next + * subtitle, or until the end of the media in the case of the last subtitle. *

* Equivalent to the UTF-8 string: " ". */ private static final byte[] SUBRIP_TIMECODE_EMPTY = new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; /** - * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. + * The value by which to divide a time in microseconds to convert it to the unit of the last value + * in a subrip timecode (milliseconds). */ - private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19; + private static long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000; /** - * The length in bytes of a timecode in a subrip prefix. + * The format of a subrip timecode. */ - private static final int SUBRIP_TIMECODE_LENGTH = 12; + private static final String SUBRIP_TIMECODE_FORMAT = "%02d:%02d:%02d,%03d"; + + /** + * Matroska specific format line for SSA subtitles. + */ + private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, " + + "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); + /** + * A template for the prefix that must be added to each SSA sample. The 10 byte end timecode + * starting at {@link #SSA_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be + * replaced with the duration of the subtitle. + *

+ * Equivalent to the UTF-8 string: "Dialogue: 0:00:00:00,0:00:00:00,". + */ + private static final byte[] SSA_PREFIX = new byte[] {68, 105, 97, 108, 111, 103, 117, 101, 58, 32, + 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44}; + /** + * The byte offset of the end timecode in {@link #SSA_PREFIX}. + */ + private static final int SSA_PREFIX_END_TIMECODE_OFFSET = 21; + /** + * The value by which to divide a time in microseconds to convert it to the unit of the last value + * in an SSA timecode (1/100ths of a second). + */ + private static long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000; + /** + * A special end timecode indicating that an SSA subtitle should be displayed until the next + * subtitle, or until the end of the media in the case of the last subtitle. + *

+ * Equivalent to the UTF-8 string: " ". + */ + private static final byte[] SSA_TIMECODE_EMPTY = + new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; + /** + * The format of an SSA timecode. + */ + private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d"; /** * The length in bytes of a WAVEFORMATEX structure. @@ -271,7 +313,7 @@ public final class MatroskaExtractor implements Extractor { private final ParsableByteArray vorbisNumPageSamples; private final ParsableByteArray seekEntryIdBytes; private final ParsableByteArray sampleStrippedBytes; - private final ParsableByteArray subripSample; + private final ParsableByteArray subtitleSample; private final ParsableByteArray encryptionInitializationVector; private final ParsableByteArray encryptionSubsampleData; private ByteBuffer encryptionSubsampleDataBuffer; @@ -349,7 +391,7 @@ public final class MatroskaExtractor implements Extractor { nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); sampleStrippedBytes = new ParsableByteArray(); - subripSample = new ParsableByteArray(); + subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); } @@ -583,11 +625,11 @@ public final class MatroskaExtractor implements Extractor { break; case ID_CONTENT_ENCODING: if (currentTrack.hasContentEncryption) { - if (currentTrack.encryptionKeyId == null) { + if (currentTrack.cryptoData == null) { throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); } - currentTrack.drmInitData = new DrmInitData( - new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId)); + currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, null, + MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey)); } break; case ID_CONTENT_ENCODINGS: @@ -891,8 +933,10 @@ public final class MatroskaExtractor implements Extractor { input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize); break; case ID_CONTENT_ENCRYPTION_KEY_ID: - currentTrack.encryptionKeyId = new byte[contentSize]; - input.readFully(currentTrack.encryptionKeyId, 0, contentSize); + byte[] encryptionKey = new byte[contentSize]; + input.readFully(encryptionKey, 0, contentSize); + currentTrack.cryptoData = new TrackOutput.CryptoData(C.CRYPTO_MODE_AES_CTR, encryptionKey, + 0, 0); // We assume patternless AES-CTR. break; case ID_SIMPLE_BLOCK: case ID_BLOCK: @@ -1014,7 +1058,7 @@ public final class MatroskaExtractor implements Extractor { // For SimpleBlock, we have metadata for each sample here. while (blockLacingSampleIndex < blockLacingSampleCount) { writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]); - long sampleTimeUs = this.blockTimeUs + long sampleTimeUs = blockTimeUs + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000; commitSampleToOutput(track, sampleTimeUs); blockLacingSampleIndex++; @@ -1034,9 +1078,13 @@ public final class MatroskaExtractor implements Extractor { private void commitSampleToOutput(Track track, long timeUs) { if (CODEC_ID_SUBRIP.equals(track.codecId)) { - writeSubripSample(track); + commitSubtitleSample(track, SUBRIP_TIMECODE_FORMAT, SUBRIP_PREFIX_END_TIMECODE_OFFSET, + SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, SUBRIP_TIMECODE_EMPTY); + } else if (CODEC_ID_ASS.equals(track.codecId)) { + commitSubtitleSample(track, SSA_TIMECODE_FORMAT, SSA_PREFIX_END_TIMECODE_OFFSET, + SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY); } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId); + track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); sampleRead = true; resetSample(); } @@ -1074,17 +1122,10 @@ public final class MatroskaExtractor implements Extractor { private void writeSampleData(ExtractorInput input, Track track, int size) throws IOException, InterruptedException { if (CODEC_ID_SUBRIP.equals(track.codecId)) { - int sizeWithPrefix = SUBRIP_PREFIX.length + size; - if (subripSample.capacity() < sizeWithPrefix) { - // Initialize subripSample to contain the required prefix and have space to hold a subtitle - // twice as long as this one. - subripSample.data = Arrays.copyOf(SUBRIP_PREFIX, sizeWithPrefix + size); - } - input.readFully(subripSample.data, SUBRIP_PREFIX.length, size); - subripSample.setPosition(0); - subripSample.setLimit(sizeWithPrefix); - // Defer writing the data to the track output. We need to modify the sample data by setting - // the correct end timecode, which we might not have yet. + writeSubtitleSampleData(input, SUBRIP_PREFIX, size); + return; + } else if (CODEC_ID_ASS.equals(track.codecId)) { + writeSubtitleSampleData(input, SSA_PREFIX, size); return; } @@ -1228,31 +1269,50 @@ public final class MatroskaExtractor implements Extractor { } } - private void writeSubripSample(Track track) { - setSubripSampleEndTimecode(subripSample.data, blockDurationUs); - // Note: If we ever want to support DRM protected subtitles then we'll need to output the - // appropriate encryption data here. - track.output.sampleData(subripSample, subripSample.limit()); - sampleBytesWritten += subripSample.limit(); + private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size) + throws IOException, InterruptedException { + int sizeWithPrefix = samplePrefix.length + size; + if (subtitleSample.capacity() < sizeWithPrefix) { + // Initialize subripSample to contain the required prefix and have space to hold a subtitle + // twice as long as this one. + subtitleSample.data = Arrays.copyOf(samplePrefix, sizeWithPrefix + size); + } else { + System.arraycopy(samplePrefix, 0, subtitleSample.data, 0, samplePrefix.length); + } + input.readFully(subtitleSample.data, samplePrefix.length, size); + subtitleSample.reset(sizeWithPrefix); + // Defer writing the data to the track output. We need to modify the sample data by setting + // the correct end timecode, which we might not have yet. } - private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) { + private void commitSubtitleSample(Track track, String timecodeFormat, int endTimecodeOffset, + long lastTimecodeValueScalingFactor, byte[] emptyTimecode) { + setSampleDuration(subtitleSample.data, blockDurationUs, timecodeFormat, endTimecodeOffset, + lastTimecodeValueScalingFactor, emptyTimecode); + // Note: If we ever want to support DRM protected subtitles then we'll need to output the + // appropriate encryption data here. + track.output.sampleData(subtitleSample, subtitleSample.limit()); + sampleBytesWritten += subtitleSample.limit(); + } + + private static void setSampleDuration(byte[] subripSampleData, long durationUs, + String timecodeFormat, int endTimecodeOffset, long lastTimecodeValueScalingFactor, + byte[] emptyTimecode) { byte[] timeCodeData; - if (timeUs == C.TIME_UNSET) { - timeCodeData = SUBRIP_TIMECODE_EMPTY; + if (durationUs == C.TIME_UNSET) { + timeCodeData = emptyTimecode; } else { - int hours = (int) (timeUs / 3600000000L); - timeUs -= (hours * 3600000000L); - int minutes = (int) (timeUs / 60000000); - timeUs -= (minutes * 60000000); - int seconds = (int) (timeUs / 1000000); - timeUs -= (seconds * 1000000); - int milliseconds = (int) (timeUs / 1000); - timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, "%02d:%02d:%02d,%03d", hours, - minutes, seconds, milliseconds)); + int hours = (int) (durationUs / (3600 * C.MICROS_PER_SECOND)); + durationUs -= (hours * 3600 * C.MICROS_PER_SECOND); + int minutes = (int) (durationUs / (60 * C.MICROS_PER_SECOND)); + durationUs -= (minutes * 60 * C.MICROS_PER_SECOND); + int seconds = (int) (durationUs / C.MICROS_PER_SECOND); + durationUs -= (seconds * C.MICROS_PER_SECOND); + int lastValue = (int) (durationUs / lastTimecodeValueScalingFactor); + timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes, + seconds, lastValue)); } - System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET, - SUBRIP_TIMECODE_LENGTH); + System.arraycopy(timeCodeData, 0, subripSampleData, endTimecodeOffset, emptyTimecode.length); } /** @@ -1383,6 +1443,7 @@ public final class MatroskaExtractor implements Extractor { || CODEC_ID_ACM.equals(codecId) || CODEC_ID_PCM_INT_LIT.equals(codecId) || CODEC_ID_SUBRIP.equals(codecId) + || CODEC_ID_ASS.equals(codecId) || CODEC_ID_VOBSUB.equals(codecId) || CODEC_ID_PGS.equals(codecId) || CODEC_ID_DVBSUB.equals(codecId); @@ -1473,7 +1534,7 @@ public final class MatroskaExtractor implements Extractor { public int defaultSampleDurationNs; public boolean hasContentEncryption; public byte[] sampleStrippedBytes; - public byte[] encryptionKeyId; + public TrackOutput.CryptoData cryptoData; public byte[] codecPrivate; public DrmInitData drmInitData; @@ -1648,6 +1709,9 @@ public final class MatroskaExtractor implements Extractor { case CODEC_ID_SUBRIP: mimeType = MimeTypes.APPLICATION_SUBRIP; break; + case CODEC_ID_ASS: + mimeType = MimeTypes.TEXT_SSA; + break; case CODEC_ID_VOBSUB: mimeType = MimeTypes.APPLICATION_VOBSUB; initializationData = Collections.singletonList(codecPrivate); @@ -1698,8 +1762,16 @@ public final class MatroskaExtractor implements Extractor { drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; + format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags, + language, drmInitData); + } else if (MimeTypes.TEXT_SSA.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; + initializationData = new ArrayList<>(2); + initializationData.add(SSA_DIALOGUE_FORMAT); + initializationData.add(codecPrivate); format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, selectionFlags, language, drmInitData); + Format.NO_VALUE, selectionFlags, language, Format.NO_VALUE, drmInitData, + Format.OFFSET_SAMPLE_RELATIVE, initializationData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType) || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 474ba65d86..f7e3e846e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -615,10 +615,10 @@ import java.util.List; || childAtomType == Atom.TYPE_wvtt || childAtomType == Atom.TYPE_stpp || childAtomType == Atom.TYPE_c608) { parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, - language, drmInitData, out); + language, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = Format.createSampleFormat(Integer.toString(trackId), - MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData); + MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, null); } stsd.setPosition(childStartPosition + childAtomSize); } @@ -626,8 +626,7 @@ import java.util.List; } private static void parseTextSampleEntry(ParsableByteArray parent, int atomType, int position, - int atomSize, int trackId, String language, DrmInitData drmInitData, StsdData out) - throws ParserException { + int atomSize, int trackId, String language, StsdData out) throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); // Default values. @@ -658,8 +657,7 @@ import java.util.List; } out.format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, 0, language, Format.NO_VALUE, drmInitData, subsampleOffsetUs, - initializationData); + Format.NO_VALUE, 0, language, Format.NO_VALUE, null, subsampleOffsetUs, initializationData); } private static void parseVideoSampleEntry(ParsableByteArray parent, int atomType, int position, @@ -676,9 +674,20 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_encv) { - atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); + Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData == null ? null + : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; + } parent.setPosition(childPosition); } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } List initializationData = null; String mimeType = null; @@ -845,9 +854,20 @@ import java.util.List; int childPosition = parent.getPosition(); if (atomType == Atom.TYPE_enca) { - atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex); + Pair sampleEntryEncryptionData = parseSampleEntryEncryptionData( + parent, position, size); + if (sampleEntryEncryptionData != null) { + atomType = sampleEntryEncryptionData.first; + drmInitData = drmInitData == null ? null + : drmInitData.copyWithSchemeType(sampleEntryEncryptionData.second.schemeType); + out.trackEncryptionBoxes[entryIndex] = sampleEntryEncryptionData.second; + } parent.setPosition(childPosition); } + // TODO: Uncomment when [Internal: b/63092960] is fixed. + // else { + // drmInitData = null; + // } // If the atom type determines a MIME type, set it immediately. String mimeType = null; @@ -1023,11 +1043,12 @@ import java.util.List; } /** - * Parses encryption data from an audio/video sample entry, populating {@code out} and returning - * the unencrypted atom type, or 0 if no common encryption sinf atom was present. + * Parses encryption data from an audio/video sample entry, returning a pair consisting of the + * unencrypted atom type and a {@link TrackEncryptionBox}. Null is returned if no common + * encryption sinf atom was present. */ - private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position, - int size, StsdData out, int entryIndex) { + private static Pair parseSampleEntryEncryptionData( + ParsableByteArray parent, int position, int size) { int childPosition = parent.getPosition(); while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1038,22 +1059,20 @@ import java.util.List; Pair result = parseSinfFromParent(parent, childPosition, childAtomSize); if (result != null) { - out.trackEncryptionBoxes[entryIndex] = result.second; - return result.first; + return result; } } childPosition += childAtomSize; } - // This enca/encv box does not have a data format so return an invalid atom type. - return 0; + return null; } private static Pair parseSinfFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; - - boolean isCencScheme = false; - TrackEncryptionBox trackEncryptionBox = null; + int schemeInformationBoxPosition = C.POSITION_UNSET; + int schemeInformationBoxSize = 0; + String schemeType = null; Integer dataFormat = null; while (childPosition - position < size) { parent.setPosition(childPosition); @@ -1063,36 +1082,60 @@ import java.util.List; dataFormat = parent.readInt(); } else if (childAtomType == Atom.TYPE_schm) { parent.skipBytes(4); - isCencScheme = parent.readInt() == TYPE_cenc; + // scheme_type field. Defined in ISO/IEC 23001-7:2016, section 4.1. + schemeType = parent.readString(4); } else if (childAtomType == Atom.TYPE_schi) { - trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); + schemeInformationBoxPosition = childPosition; + schemeInformationBoxSize = childAtomSize; } childPosition += childAtomSize; } - if (isCencScheme) { + if (schemeType != null) { Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); - Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory"); - return Pair.create(dataFormat, trackEncryptionBox); + Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, + "schi atom is mandatory"); + TrackEncryptionBox encryptionBox = parseSchiFromParent(parent, schemeInformationBoxPosition, + schemeInformationBoxSize, schemeType); + Assertions.checkArgument(encryptionBox != null, "tenc atom is mandatory"); + return Pair.create(dataFormat, encryptionBox); } else { return null; } } private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, - int size) { + int size, String schemeType) { int childPosition = position + Atom.HEADER_SIZE; while (childPosition - position < size) { parent.setPosition(childPosition); int childAtomSize = parent.readInt(); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_tenc) { - parent.skipBytes(6); - boolean defaultIsEncrypted = parent.readUnsignedByte() == 1; - int defaultInitVectorSize = parent.readUnsignedByte(); + int fullAtom = parent.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + parent.skipBytes(1); // reserved = 0. + int defaultCryptByteBlock = 0; + int defaultSkipByteBlock = 0; + if (version == 0) { + parent.skipBytes(1); // reserved = 0. + } else /* version 1 or greater */ { + int patternByte = parent.readUnsignedByte(); + defaultCryptByteBlock = (patternByte & 0xF0) >> 4; + defaultSkipByteBlock = patternByte & 0x0F; + } + boolean defaultIsProtected = parent.readUnsignedByte() == 1; + int defaultPerSampleIvSize = parent.readUnsignedByte(); byte[] defaultKeyId = new byte[16]; parent.readBytes(defaultKeyId, 0, defaultKeyId.length); - return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId); + byte[] constantIv = null; + if (defaultIsProtected && defaultPerSampleIvSize == 0) { + int constantIvSize = parent.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + parent.readBytes(constantIv, 0, constantIvSize); + } + return new TrackEncryptionBox(defaultIsProtected, schemeType, defaultPerSampleIvSize, + defaultKeyId, defaultCryptByteBlock, defaultSkipByteBlock, constantIv); } childPosition += childAtomSize; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index a228a9b775..6b2077ef76 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -128,6 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray nalPrefix; private final ParsableByteArray nalBuffer; private final ParsableByteArray encryptionSignalByte; + private final ParsableByteArray defaultInitializationVector; // Adjusts sample timestamps. private final TimestampAdjuster timestampAdjuster; @@ -197,6 +198,7 @@ public final class FragmentedMp4Extractor implements Extractor { nalPrefix = new ParsableByteArray(5); nalBuffer = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); + defaultInitializationVector = new ParsableByteArray(); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); pendingMetadataSampleInfos = new LinkedList<>(); @@ -462,8 +464,8 @@ public final class FragmentedMp4Extractor implements Extractor { if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, C.TRACK_TYPE_TEXT); - cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, - null, Format.NO_VALUE, 0, null, null)); + cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, + null)); cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; } } @@ -559,11 +561,12 @@ public final class FragmentedMp4Extractor implements Extractor { parseTruns(traf, trackBundle, decodeTime, flags); + TrackEncryptionBox encryptionBox = trackBundle.track + .getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); if (saiz != null) { - TrackEncryptionBox trackEncryptionBox = trackBundle.track - .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex]; - parseSaiz(trackEncryptionBox, saiz.data, fragment); + parseSaiz(encryptionBox, saiz.data, fragment); } LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); @@ -579,7 +582,8 @@ public final class FragmentedMp4Extractor implements Extractor { LeafAtom sbgp = traf.getLeafAtomOfType(Atom.TYPE_sbgp); LeafAtom sgpd = traf.getLeafAtomOfType(Atom.TYPE_sgpd); if (sbgp != null && sgpd != null) { - parseSgpd(sbgp.data, sgpd.data, fragment); + parseSgpd(sbgp.data, sgpd.data, encryptionBox != null ? encryptionBox.schemeType : null, + fragment); } int leafChildrenSize = traf.leafChildren.size(); @@ -811,7 +815,7 @@ public final class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale); + sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000L) / timescale); } else { sampleCompositionTimeOffsetTable[i] = 0; } @@ -868,8 +872,8 @@ public final class FragmentedMp4Extractor implements Extractor { out.fillEncryptionData(senc); } - private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, TrackFragment out) - throws ParserException { + private static void parseSgpd(ParsableByteArray sbgp, ParsableByteArray sgpd, String schemeType, + TrackFragment out) throws ParserException { sbgp.setPosition(Atom.HEADER_SIZE); int sbgpFullAtom = sbgp.readInt(); if (sbgp.readInt() != SAMPLE_GROUP_TYPE_seig) { @@ -877,9 +881,9 @@ public final class FragmentedMp4Extractor implements Extractor { return; } if (Atom.parseFullAtomVersion(sbgpFullAtom) == 1) { - sbgp.skipBytes(4); + sbgp.skipBytes(4); // default_length. } - if (sbgp.readInt() != 1) { + if (sbgp.readInt() != 1) { // entry_count. throw new ParserException("Entry count in sbgp != 1 (unsupported)."); } @@ -892,25 +896,35 @@ public final class FragmentedMp4Extractor implements Extractor { int sgpdVersion = Atom.parseFullAtomVersion(sgpdFullAtom); if (sgpdVersion == 1) { if (sgpd.readUnsignedInt() == 0) { - throw new ParserException("Variable length decription in sgpd found (unsupported)"); + throw new ParserException("Variable length description in sgpd found (unsupported)"); } } else if (sgpdVersion >= 2) { - sgpd.skipBytes(4); + sgpd.skipBytes(4); // default_sample_description_index. } - if (sgpd.readUnsignedInt() != 1) { + if (sgpd.readUnsignedInt() != 1) { // entry_count. throw new ParserException("Entry count in sgpd != 1 (unsupported)."); } // CencSampleEncryptionInformationGroupEntry - sgpd.skipBytes(2); + sgpd.skipBytes(1); // reserved = 0. + int patternByte = sgpd.readUnsignedByte(); + int cryptByteBlock = (patternByte & 0xF0) >> 4; + int skipByteBlock = patternByte & 0x0F; boolean isProtected = sgpd.readUnsignedByte() == 1; if (!isProtected) { return; } - int initVectorSize = sgpd.readUnsignedByte(); + int perSampleIvSize = sgpd.readUnsignedByte(); byte[] keyId = new byte[16]; sgpd.readBytes(keyId, 0, keyId.length); + byte[] constantIv = null; + if (isProtected && perSampleIvSize == 0) { + int constantIvSize = sgpd.readUnsignedByte(); + constantIv = new byte[constantIvSize]; + sgpd.readBytes(constantIv, 0, constantIvSize); + } out.definesEncryptionData = true; - out.trackEncryptionBox = new TrackEncryptionBox(isProtected, initVectorSize, keyId); + out.trackEncryptionBox = new TrackEncryptionBox(isProtected, schemeType, perSampleIvSize, keyId, + cryptByteBlock, skipByteBlock, constantIv); } /** @@ -1122,19 +1136,24 @@ public final class FragmentedMp4Extractor implements Extractor { } long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; - @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0) - | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0); - int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; - byte[] encryptionKey = null; - if (fragment.definesEncryptionData) { - encryptionKey = fragment.trackEncryptionBox != null - ? fragment.trackEncryptionBox.keyId - : track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId; - } if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } - output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); + + @C.BufferFlags int sampleFlags = fragment.sampleIsSyncFrameTable[sampleIndex] + ? C.BUFFER_FLAG_KEY_FRAME : 0; + + // Encryption data. + TrackOutput.CryptoData cryptoData = null; + if (fragment.definesEncryptionData) { + sampleFlags |= C.BUFFER_FLAG_ENCRYPTED; + TrackEncryptionBox encryptionBox = fragment.trackEncryptionBox != null + ? fragment.trackEncryptionBox + : track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + cryptoData = encryptionBox.cryptoData; + } + + output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); while (!pendingMetadataSampleInfos.isEmpty()) { MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); @@ -1190,12 +1209,24 @@ public final class FragmentedMp4Extractor implements Extractor { */ private int appendSampleEncryptionData(TrackBundle trackBundle) { TrackFragment trackFragment = trackBundle.fragment; - ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData; int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex; TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null ? trackFragment.trackEncryptionBox - : trackBundle.track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; - int vectorSize = encryptionBox.initializationVectorSize; + : trackBundle.track.getSampleDescriptionEncryptionBox(sampleDescriptionIndex); + + ParsableByteArray initializationVectorData; + int vectorSize; + if (encryptionBox.initializationVectorSize != 0) { + initializationVectorData = trackFragment.sampleEncryptionData; + vectorSize = encryptionBox.initializationVectorSize; + } else { + // The default initialization vector should be used. + byte[] initVectorData = encryptionBox.defaultInitializationVector; + defaultInitializationVector.reset(initVectorData, initVectorData.length); + initializationVectorData = defaultInitializationVector; + vectorSize = initVectorData.length; + } + boolean subsampleEncryption = trackFragment .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex]; @@ -1205,20 +1236,20 @@ public final class FragmentedMp4Extractor implements Extractor { TrackOutput output = trackBundle.output; output.sampleData(encryptionSignalByte, 1); // Write the vector. - output.sampleData(sampleEncryptionData, vectorSize); + output.sampleData(initializationVectorData, vectorSize); // If we don't have subsample encryption data, we're done. if (!subsampleEncryption) { return 1 + vectorSize; } // Write the subsample encryption data. - int subsampleCount = sampleEncryptionData.readUnsignedShort(); - sampleEncryptionData.skipBytes(-2); + ParsableByteArray subsampleEncryptionData = trackFragment.sampleEncryptionData; + int subsampleCount = subsampleEncryptionData.readUnsignedShort(); + subsampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; - output.sampleData(sampleEncryptionData, subsampleDataLength); + output.sampleData(subsampleEncryptionData, subsampleDataLength); return 1 + vectorSize + subsampleDataLength; } - /** Returns DrmInitData from leaf atoms. */ private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) { ArrayList schemeDatas = null; @@ -1234,7 +1265,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (uuid == null) { Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); } else { - schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData)); + schemeDatas.add(new SchemeData(uuid, null, MimeTypes.VIDEO_MP4, psshData)); } } } @@ -1308,8 +1339,12 @@ public final class FragmentedMp4Extractor implements Extractor { } public void updateDrmInitData(DrmInitData drmInitData) { - output.format(track.format.copyWithDrmInitData(drmInitData)); + TrackEncryptionBox encryptionBox = + track.getSampleDescriptionEncryptionBox(fragment.header.sampleDescriptionIndex); + String schemeType = encryptionBox != null ? encryptionBox.schemeType : null; + output.format(track.format.copyWithDrmInitData(drmInitData.copyWithSchemeType(schemeType))); } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index f1c4e99ec1..7ac3158794 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import java.lang.annotation.Retention; @@ -77,11 +78,6 @@ public final class Track { */ @Transformation public final int sampleTransformation; - /** - * Track encryption boxes for the different track sample descriptions. Entries may be null. - */ - public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; - /** * Durations of edit list segments in the movie timescale. Null if there is no edit list. */ @@ -98,9 +94,11 @@ public final class Track { */ public final int nalUnitLengthFieldLength; + @Nullable private final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; + public Track(int id, int type, long timescale, long movieTimescale, long durationUs, Format format, @Transformation int sampleTransformation, - TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, + @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, long[] editListDurations, long[] editListMediaTimes) { this.id = id; this.type = type; @@ -115,4 +113,16 @@ public final class Track { this.editListMediaTimes = editListMediaTimes; } + /** + * Returns the {@link TrackEncryptionBox} for the given sample description index. + * + * @param sampleDescriptionIndex The given sample description index + * @return The {@link TrackEncryptionBox} for the given sample description index. Maybe null if no + * such entry exists. + */ + public TrackEncryptionBox getSampleDescriptionEncryptionBox(int sampleDescriptionIndex) { + return sampleDescriptionEncryptionBoxes == null ? null + : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java index dde03a8507..d39aae0c5f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java @@ -15,37 +15,86 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import android.support.annotation.Nullable; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.Assertions; + /** * Encapsulates information parsed from a track encryption (tenc) box or sample group description * (sgpd) box in an MP4 stream. */ public final class TrackEncryptionBox { + private static final String TAG = "TrackEncryptionBox"; + /** * Indicates the encryption state of the samples in the sample group. */ public final boolean isEncrypted; + /** + * The protection scheme type, as defined by the 'schm' box, or null if unknown. + */ + @Nullable public final String schemeType; + + /** + * A {@link TrackOutput.CryptoData} instance containing the encryption information from this + * {@link TrackEncryptionBox}. + */ + public final TrackOutput.CryptoData cryptoData; + /** * The initialization vector size in bytes for the samples in the corresponding sample group. */ public final int initializationVectorSize; /** - * The key identifier for the samples in the corresponding sample group. + * If {@link #initializationVectorSize} is 0, holds the default initialization vector as defined + * in the track encryption box or sample group description box. Null otherwise. */ - public final byte[] keyId; + public final byte[] defaultInitializationVector; /** - * @param isEncrypted Indicates the encryption state of the samples in the sample group. - * @param initializationVectorSize The initialization vector size in bytes for the samples in the - * corresponding sample group. - * @param keyId The key identifier for the samples in the corresponding sample group. + * @param isEncrypted See {@link #isEncrypted}. + * @param schemeType See {@link #schemeType}. + * @param initializationVectorSize See {@link #initializationVectorSize}. + * @param keyId See {@link TrackOutput.CryptoData#encryptionKey}. + * @param defaultEncryptedBlocks See {@link TrackOutput.CryptoData#encryptedBlocks}. + * @param defaultClearBlocks See {@link TrackOutput.CryptoData#clearBlocks}. + * @param defaultInitializationVector See {@link #defaultInitializationVector}. */ - public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) { + public TrackEncryptionBox(boolean isEncrypted, @Nullable String schemeType, + int initializationVectorSize, byte[] keyId, int defaultEncryptedBlocks, + int defaultClearBlocks, @Nullable byte[] defaultInitializationVector) { + Assertions.checkArgument(initializationVectorSize == 0 ^ defaultInitializationVector == null); this.isEncrypted = isEncrypted; + this.schemeType = schemeType; this.initializationVectorSize = initializationVectorSize; - this.keyId = keyId; + this.defaultInitializationVector = defaultInitializationVector; + cryptoData = new TrackOutput.CryptoData(schemeToCryptoMode(schemeType), keyId, + defaultEncryptedBlocks, defaultClearBlocks); + } + + @C.CryptoMode + private static int schemeToCryptoMode(@Nullable String schemeType) { + if (schemeType == null) { + // If unknown, assume cenc. + return C.CRYPTO_MODE_AES_CTR; + } + switch (schemeType) { + case C.CENC_TYPE_cenc: + case C.CENC_TYPE_cens: + return C.CRYPTO_MODE_AES_CTR; + case C.CENC_TYPE_cbc1: + case C.CENC_TYPE_cbcs: + return C.CRYPTO_MODE_AES_CBC; + default: + Log.w(TAG, "Unsupported protection scheme type '" + schemeType + "'. Assuming AES-CTR " + + "crypto mode."); + return C.CRYPTO_MODE_AES_CTR; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index cc3c5de311..54e168c665 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -45,30 +45,14 @@ public class OggExtractor implements Extractor { private static final int MAX_VERIFICATION_BYTES = 8; + private ExtractorOutput output; private StreamReader streamReader; + private boolean streamReaderInitialized; @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { try { - OggPageHeader header = new OggPageHeader(); - if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { - return false; - } - - int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); - ParsableByteArray scratch = new ParsableByteArray(length); - input.peekFully(scratch.data, 0, length); - - if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new FlacReader(); - } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new VorbisReader(); - } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new OpusReader(); - } else { - return false; - } - return true; + return sniffInternal(input); } catch (ParserException e) { return false; } @@ -76,15 +60,14 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); - output.endTracks(); - // TODO: fix the case if sniff() isn't called - streamReader.init(output, trackOutput); + this.output = output; } @Override public void seek(long position, long timeUs) { - streamReader.seek(position, timeUs); + if (streamReader != null) { + streamReader.seek(position, timeUs); + } } @Override @@ -95,12 +78,41 @@ public class OggExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + if (streamReader == null) { + if (!sniffInternal(input)) { + throw new ParserException("Failed to determine bitstream type"); + } + input.resetPeekPosition(); + } + if (!streamReaderInitialized) { + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + streamReader.init(output, trackOutput); + streamReaderInitialized = true; + } return streamReader.read(input, seekPosition); } - //@VisibleForTesting - /* package */ StreamReader getStreamReader() { - return streamReader; + private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException { + OggPageHeader header = new OggPageHeader(); + if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { + return false; + } + + int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); + ParsableByteArray scratch = new ParsableByteArray(length); + input.peekFully(scratch.data, 0, length); + + if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new FlacReader(); + } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new VorbisReader(); + } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new OpusReader(); + } else { + return false; + } + return true; } private static ParsableByteArray resetPosition(ParsableByteArray scratch) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java index c203b0c6bd..d136468faa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/StreamReader.java @@ -41,7 +41,8 @@ import java.io.IOException; OggSeeker oggSeeker; } - private OggPacket oggPacket; + private final OggPacket oggPacket; + private TrackOutput trackOutput; private ExtractorOutput extractorOutput; private OggSeeker oggSeeker; @@ -55,11 +56,13 @@ import java.io.IOException; private boolean seekMapSet; private boolean formatSet; + public StreamReader() { + oggPacket = new OggPacket(); + } + void init(ExtractorOutput output, TrackOutput trackOutput) { this.extractorOutput = output; this.trackOutput = trackOutput; - this.oggPacket = new OggPacket(); - reset(true); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 9c9536beec..8bab6b7ed1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -56,9 +56,9 @@ public final class Ac3Extractor implements Extractor { private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); private final long firstSampleTimestampUs; + private final Ac3Reader reader; private final ParsableByteArray sampleData; - private Ac3Reader reader; private boolean startedPacket; public Ac3Extractor() { @@ -67,6 +67,7 @@ public final class Ac3Extractor implements Extractor { public Ac3Extractor(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new Ac3Reader(); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } @@ -117,7 +118,6 @@ public final class Ac3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new Ac3Reader(); // TODO: Add support for embedded ID3. reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index f7dadd51b2..a1851aa0ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -55,10 +55,9 @@ public final class AdtsExtractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private final long firstSampleTimestampUs; + private final AdtsReader reader; private final ParsableByteArray packetBuffer; - // Accessed only by the loading thread. - private AdtsReader reader; private boolean startedPacket; public AdtsExtractor() { @@ -67,6 +66,7 @@ public final class AdtsExtractor implements Extractor { public AdtsExtractor(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); } @@ -127,7 +127,6 @@ public final class AdtsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new AdtsReader(true); reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 4bc7f11c1a..40cfd7f8d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -78,7 +78,7 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact this.flags = flags; if (!isSet(FLAG_OVERRIDE_CAPTION_DESCRIPTORS) && closedCaptionFormats.isEmpty()) { closedCaptionFormats = Collections.singletonList(Format.createTextSampleFormat(null, - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); + MimeTypes.APPLICATION_CEA608, 0, null)); } this.closedCaptionFormats = closedCaptionFormats; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 7266f847c4..a3502a3242 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -51,17 +51,17 @@ public final class H262Reader implements ElementaryStreamReader { // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; - private boolean foundFirstFrameInGroup; private long totalBytesWritten; + private boolean startedFirstSample; // Per packet state that gets reset at the start of each packet. private long pesTimeUs; - private boolean pesPtsUsAvailable; - // Per sample state that gets reset at the start of each frame. - private boolean isKeyframe; - private long framePosition; - private long frameTimeUs; + // Per sample state that gets reset at the start of each sample. + private long samplePosition; + private long sampleTimeUs; + private boolean sampleIsKeyframe; + private boolean sampleHasPicture; public H262Reader() { prefixFlags = new boolean[4]; @@ -72,9 +72,8 @@ public final class H262Reader implements ElementaryStreamReader { public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - pesPtsUsAvailable = false; - foundFirstFrameInGroup = false; totalBytesWritten = 0; + startedFirstSample = false; } @Override @@ -86,10 +85,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET; - if (pesPtsUsAvailable) { - this.pesTimeUs = pesTimeUs; - } + this.pesTimeUs = pesTimeUs; } @Override @@ -102,9 +98,8 @@ public final class H262Reader implements ElementaryStreamReader { totalBytesWritten += data.bytesLeft(); output.sampleData(data, data.bytesLeft()); - int searchOffset = offset; while (true) { - int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, limit, prefixFlags); + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); if (startCodeOffset == limit) { // We've scanned to the end of the data without finding another start code. @@ -125,7 +120,7 @@ public final class H262Reader implements ElementaryStreamReader { csdBuffer.onData(dataArray, offset, startCodeOffset); } // This is the number of bytes belonging to the next start code that have already been - // passed to csdDataTargetBuffer. + // passed to csdBuffer. int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. @@ -136,27 +131,29 @@ public final class H262Reader implements ElementaryStreamReader { } } - if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) { + if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) { int bytesWrittenPastStartCode = limit - startCodeOffset; - if (foundFirstFrameInGroup) { - @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; - int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode; - output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null); - isKeyframe = false; + if (startedFirstSample && sampleHasPicture && hasOutputFormat) { + // Output the sample. + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; + output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null); } - if (startCodeValue == START_GROUP) { - foundFirstFrameInGroup = false; - isKeyframe = true; - } else /* startCodeValue == START_PICTURE */ { - frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs); - framePosition = totalBytesWritten - bytesWrittenPastStartCode; - pesPtsUsAvailable = false; - foundFirstFrameInGroup = true; + if (!startedFirstSample || sampleHasPicture) { + // Start the next sample. + samplePosition = totalBytesWritten - bytesWrittenPastStartCode; + sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs + : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0); + sampleIsKeyframe = false; + pesTimeUs = C.TIME_UNSET; + startedFirstSample = true; } + sampleHasPicture = startCodeValue == START_PICTURE; + } else if (startCodeValue == START_GROUP) { + sampleIsKeyframe = true; } - offset = startCodeOffset; - searchOffset = offset + 3; + offset = startCodeOffset + 3; } } @@ -221,6 +218,8 @@ public final class H262Reader implements ElementaryStreamReader { private static final class CsdBuffer { + private static final byte[] START_CODE = new byte[] {0, 0, 1}; + private boolean isFilling; public int length; @@ -244,24 +243,25 @@ public final class H262Reader implements ElementaryStreamReader { * Called when a start code is encountered in the stream. * * @param startCodeValue The start code value. - * @param bytesAlreadyPassed The number of bytes of the start code that have already been - * passed to {@link #onData(byte[], int, int)}, or 0. + * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to + * {@link #onData(byte[], int, int)}, or 0. * @return Whether the csd data is now complete. If true is returned, neither - * this method or {@link #onData(byte[], int, int)} should be called again without an + * this method nor {@link #onData(byte[], int, int)} should be called again without an * interleaving call to {@link #reset()}. */ public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { if (isFilling) { + length -= bytesAlreadyPassed; if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { sequenceExtensionPosition = length; } else { - length -= bytesAlreadyPassed; isFilling = false; return true; } } else if (startCodeValue == START_SEQUENCE_HEADER) { isFilling = true; } + onData(START_CODE, 0, START_CODE.length); return false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 1149856649..2929b8a076 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -105,8 +105,8 @@ public final class TsExtractor implements Extractor { private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); - private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2 - private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; + private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; + private static final int SNIFF_TS_PACKET_COUNT = 5; @Mode private final int mode; private final List timestampAdjusters; @@ -174,10 +174,10 @@ public final class TsExtractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { byte[] buffer = tsPacketBuffer.data; - input.peekFully(buffer, 0, BUFFER_SIZE); + input.peekFully(buffer, 0, TS_PACKET_SIZE * SNIFF_TS_PACKET_COUNT); for (int j = 0; j < TS_PACKET_SIZE; j++) { for (int i = 0; true; i++) { - if (i == BUFFER_PACKET_COUNT) { + if (i == SNIFF_TS_PACKET_COUNT) { input.skipFully(j); return true; } @@ -216,7 +216,8 @@ public final class TsExtractor implements Extractor { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { byte[] data = tsPacketBuffer.data; - // Shift bytes to the start of the buffer if there isn't enough space left at the end + + // Shift bytes to the start of the buffer if there isn't enough space left at the end. if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) { int bytesLeft = tsPacketBuffer.bytesLeft(); if (bytesLeft > 0) { @@ -224,7 +225,8 @@ public final class TsExtractor implements Extractor { } tsPacketBuffer.reset(data, bytesLeft); } - // Read more bytes until there is at least one packet size + + // Read more bytes until we have at least one packet. while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) { int limit = tsPacketBuffer.limit(); int read = input.read(data, limit, BUFFER_SIZE - limit); @@ -234,8 +236,7 @@ public final class TsExtractor implements Extractor { tsPacketBuffer.setLimit(limit + read); } - // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of - // the header. + // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. final int limit = tsPacketBuffer.limit(); int position = tsPacketBuffer.getPosition(); while (position < limit && data[position] != TS_SYNC_BYTE) { @@ -554,5 +555,4 @@ public final class TsExtractor implements Extractor { } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index a7c237edb2..2e5b04f4a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -61,6 +61,14 @@ public final class MediaCodecInfo { */ public final boolean tunneling; + /** + * Whether the decoder is secure. + * + * @see CodecCapabilities#isFeatureSupported(String) + * @see CodecCapabilities#FEATURE_SecurePlayback + */ + public final boolean secure; + private final String mimeType; private final CodecCapabilities capabilities; @@ -71,7 +79,7 @@ public final class MediaCodecInfo { * @return The created instance. */ public static MediaCodecInfo newPassthroughInstance(String name) { - return new MediaCodecInfo(name, null, null, false); + return new MediaCodecInfo(name, null, null, false, false); } /** @@ -84,7 +92,7 @@ public final class MediaCodecInfo { */ public static MediaCodecInfo newInstance(String name, String mimeType, CodecCapabilities capabilities) { - return new MediaCodecInfo(name, mimeType, capabilities, false); + return new MediaCodecInfo(name, mimeType, capabilities, false, false); } /** @@ -94,20 +102,22 @@ public final class MediaCodecInfo { * @param mimeType A mime type supported by the {@link MediaCodec}. * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. + * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. */ public static MediaCodecInfo newInstance(String name, String mimeType, - CodecCapabilities capabilities, boolean forceDisableAdaptive) { - return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive); + CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { + return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure); } private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities, - boolean forceDisableAdaptive) { + boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; this.capabilities = capabilities; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); + secure = forceSecure || (capabilities != null && isSecure(capabilities)); } /** @@ -176,12 +186,12 @@ public final class MediaCodecInfo { logNoSupport("sizeAndRate.vCaps"); return false; } - if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) { + if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) { // Capabilities are known to be inaccurately reported for vertical resolutions on some devices // (b/31387661). If the video is vertical and the capabilities indicate support if the width // and height are swapped, we assume that the vertical resolution is also supported. if (width >= height - || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) { + || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) { logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); return false; } @@ -326,14 +336,6 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); } - @TargetApi(21) - private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width, - int height, double frameRate) { - return frameRate == Format.NO_VALUE || frameRate <= 0 - ? capabilities.isSizeSupported(width, height) - : capabilities.areSizeAndRateSupported(width, height, frameRate); - } - private static boolean isTunneling(CodecCapabilities capabilities) { return Util.SDK_INT >= 21 && isTunnelingV21(capabilities); } @@ -343,4 +345,21 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback); } + private static boolean isSecure(CodecCapabilities capabilities) { + return Util.SDK_INT >= 21 && isSecureV21(capabilities); + } + + @TargetApi(21) + private static boolean isSecureV21(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); + } + + @TargetApi(21) + private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, + int height, double frameRate) { + return frameRate == Format.NO_VALUE || frameRate <= 0 + ? capabilities.isSizeSupported(width, height) + : capabilities.areSizeAndRateSupported(width, height, frameRate); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index f0e23ebfeb..01229c1104 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -23,6 +23,7 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; @@ -31,7 +32,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmInitData; 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.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; @@ -175,10 +178,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final MediaCodec.BufferInfo outputBufferInfo; private Format format; - private MediaCodec codec; private DrmSession drmSession; private DrmSession pendingDrmSession; - private boolean codecIsAdaptive; + private MediaCodec codec; + private MediaCodecInfo codecInfo; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; private boolean codecNeedsAdaptationWorkaround; @@ -237,14 +240,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } @Override - public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } @Override public final int supportsFormat(Format format) throws ExoPlaybackException { try { - return supportsFormat(mediaCodecSelector, format); + int formatSupport = supportsFormat(mediaCodecSelector, format); + if ((formatSupport & FORMAT_SUPPORT_MASK) > FORMAT_UNSUPPORTED_DRM + && !isDrmSchemeSupported(drmSessionManager, format.drmInitData)) { + // The renderer advertises higher support than FORMAT_UNSUPPORTED_DRM but the DRM scheme is + // not supported. The format support is truncated to reflect this. + formatSupport = (formatSupport & ~FORMAT_SUPPORT_MASK) | FORMAT_UNSUPPORTED_DRM; + } + return formatSupport; } catch (DecoderQueryException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -291,55 +301,60 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @SuppressWarnings("deprecation") protected final void maybeInitCodec() throws ExoPlaybackException { - if (!shouldInitCodec()) { + if (codec != null || format == null) { + // We have a codec already, or we don't have a format with which to instantiate one. return; } drmSession = pendingDrmSession; String mimeType = format.sampleMimeType; - MediaCrypto mediaCrypto = null; + MediaCrypto wrappedMediaCrypto = null; boolean drmSessionRequiresSecureDecoder = false; if (drmSession != null) { - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } else if (drmSessionState == DrmSession.STATE_OPENED - || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) { - mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto(); - drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType); - } else { + FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + } // The drm session isn't open yet. return; } + wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); + drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } - MediaCodecInfo decoderInfo = null; - try { - decoderInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); - if (decoderInfo == null && drmSessionRequiresSecureDecoder) { - // The drm session indicates that a secure decoder is required, but the device does not have - // one. Assuming that supportsFormat indicated support for the media being played, we know - // that it does not require a secure output path. Most CDM implementations allow playback to - // proceed with a non-secure decoder in this case, so we try our luck. - decoderInfo = getDecoderInfo(mediaCodecSelector, format, false); - if (decoderInfo != null) { - Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but " - + "no secure decoder available. Trying to proceed with " + decoderInfo.name + "."); + if (codecInfo == null) { + try { + codecInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder); + if (codecInfo == null && drmSessionRequiresSecureDecoder) { + // The drm session indicates that a secure decoder is required, but the device does not + // have one. Assuming that supportsFormat indicated support for the media being played, we + // know that it does not require a secure output path. Most CDM implementations allow + // playback to proceed with a non-secure decoder in this case, so we try our luck. + codecInfo = getDecoderInfo(mediaCodecSelector, format, false); + if (codecInfo != null) { + Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but " + + "no secure decoder available. Trying to proceed with " + codecInfo.name + "."); + } } + } catch (DecoderQueryException e) { + throwDecoderInitError(new DecoderInitializationException(format, e, + drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); + } + + if (codecInfo == null) { + throwDecoderInitError(new DecoderInitializationException(format, null, + drmSessionRequiresSecureDecoder, + DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); } - } catch (DecoderQueryException e) { - throwDecoderInitError(new DecoderInitializationException(format, e, - drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); } - if (decoderInfo == null) { - throwDecoderInitError(new DecoderInitializationException(format, null, - drmSessionRequiresSecureDecoder, - DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); + if (!shouldInitCodec(codecInfo)) { + return; } - String codecName = decoderInfo.name; - codecIsAdaptive = decoderInfo.adaptive; + String codecName = codecInfo.name; codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); @@ -353,7 +368,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codec = MediaCodec.createByCodecName(codecName); TraceUtil.endSection(); TraceUtil.beginSection("configureCodec"); - configureCodec(decoderInfo, codec, format, mediaCrypto); + configureCodec(codecInfo, codec, format, wrappedMediaCrypto); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); codec.start(); @@ -380,14 +395,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer(e, getIndex()); } - protected boolean shouldInitCodec() { - return codec == null && format != null; + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return true; } protected final MediaCodec getCodec() { return codec; } + protected final MediaCodecInfo getCodecInfo() { + return codecInfo; + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); @@ -426,31 +445,31 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } protected void releaseCodec() { + codecHotswapDeadlineMs = C.TIME_UNSET; + inputIndex = C.INDEX_UNSET; + outputIndex = C.INDEX_UNSET; + waitingForKeys = false; + shouldSkipOutputBuffer = false; + decodeOnlyPresentationTimestamps.clear(); + inputBuffers = null; + outputBuffers = null; + codecInfo = null; + codecReconfigured = false; + codecReceivedBuffers = false; + codecNeedsDiscardToSpsWorkaround = false; + codecNeedsFlushWorkaround = false; + codecNeedsAdaptationWorkaround = false; + codecNeedsEosPropagationWorkaround = false; + codecNeedsEosFlushWorkaround = false; + codecNeedsMonoChannelCountWorkaround = false; + codecNeedsAdaptationWorkaroundBuffer = false; + shouldSkipAdaptationWorkaroundOutputBuffer = false; + codecReceivedEos = false; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecReinitializationState = REINITIALIZATION_STATE_NONE; + buffer.data = null; if (codec != null) { - codecHotswapDeadlineMs = C.TIME_UNSET; - inputIndex = C.INDEX_UNSET; - outputIndex = C.INDEX_UNSET; - waitingForKeys = false; - shouldSkipOutputBuffer = false; - decodeOnlyPresentationTimestamps.clear(); - inputBuffers = null; - outputBuffers = null; - codecReconfigured = false; - codecReceivedBuffers = false; - codecIsAdaptive = false; - codecNeedsDiscardToSpsWorkaround = false; - codecNeedsFlushWorkaround = false; - codecNeedsAdaptationWorkaround = false; - codecNeedsEosPropagationWorkaround = false; - codecNeedsEosFlushWorkaround = false; - codecNeedsMonoChannelCountWorkaround = false; - codecNeedsAdaptationWorkaroundBuffer = false; - shouldSkipAdaptationWorkaroundOutputBuffer = false; - codecReceivedEos = false; - codecReconfigurationState = RECONFIGURATION_STATE_NONE; - codecReinitializationState = REINITIALIZATION_STATE_NONE; decoderCounters.decoderReleaseCount++; - buffer.data = null; try { codec.stop(); } finally { @@ -727,15 +746,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null) { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } @DrmSession.State int drmSessionState = drmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS - && (bufferEncrypted || !playClearSamplesWithoutKeys); + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } /** @@ -781,7 +799,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (pendingDrmSession == drmSession && codec != null - && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) { + && canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) { codecReconfigured = true; codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround @@ -1065,6 +1083,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + /** + * Returns whether the encryption scheme is supported, or true if {@code drmInitData} is null. + * + * @param drmSessionManager The drm session manager associated with the renderer. + * @param drmInitData {@link DrmInitData} of the format to check for support. + * @return Whether the encryption scheme is supported, or true if {@code drmInitData} is null. + */ + private static boolean isDrmSchemeSupported(DrmSessionManager drmSessionManager, + @Nullable DrmInitData drmInitData) { + if (drmInitData == null) { + // Content is unencrypted. + return true; + } else if (drmSessionManager == null) { + // Content is encrypted, but no drm session manager is available. + return false; + } + return drmSessionManager.canAcquireSession(drmInitData); + } + /** * Returns whether the decoder is known to fail when flushed. *

diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java index bb946d76f9..1823c3a7ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -55,8 +55,7 @@ public interface MediaCodecSelector { /** * Selects a decoder to instantiate for audio passthrough. * - * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder - * exists. + * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists. * @throws DecoderQueryException Thrown if there was an error querying decoders. */ MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 392162f607..d3f3dae344 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -101,7 +101,7 @@ public final class MediaCodecUtil { /** * Returns information about a decoder suitable for audio passthrough. - ** + * * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder * exists. */ @@ -233,10 +233,10 @@ public final class MediaCodecUtil { if ((secureDecodersExplicit && key.secure == secure) || (!secureDecodersExplicit && !key.secure)) { decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities, - forceDisableAdaptive)); + forceDisableAdaptive, false)); } else if (!secureDecodersExplicit && secure) { decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType, - capabilities, forceDisableAdaptive)); + capabilities, forceDisableAdaptive, true)); // It only makes sense to have one synthesized secure decoder, return immediately. return decoderInfos; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 70b2d8aab9..7ff426e2df 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -104,7 +104,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { decoder = decoderFactory.createDecoder(formats[0]); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index df3353fb18..6b2e5c3675 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -483,13 +483,8 @@ public final class Id3Decoder implements MetadataDecoder { int ownerEndIndex = indexOfZeroByte(data, 0); String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); - byte[] privateData; int privateDataStartIndex = ownerEndIndex + 1; - if (privateDataStartIndex < data.length) { - privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); - } else { - privateData = new byte[0]; - } + byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length); return new PrivFrame(owner, privateData); } @@ -516,7 +511,7 @@ public final class Id3Decoder implements MetadataDecoder { descriptionEndIndex - descriptionStartIndex, charset); int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] objectData = Arrays.copyOfRange(data, objectDataStartIndex, data.length); + byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length); return new GeobFrame(mimeType, filename, description, objectData); } @@ -553,7 +548,7 @@ public final class Id3Decoder implements MetadataDecoder { descriptionEndIndex - descriptionStartIndex, charset); int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length); + byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length); return new ApicFrame(mimeType, description, pictureType, pictureData); } @@ -749,6 +744,22 @@ public final class Id3Decoder implements MetadataDecoder { ? 1 : 2; } + /** + * Copies the specified range of an array, or returns a zero length array if the range is invalid. + * + * @param data The array from which to copy. + * @param from The start of the range to copy (inclusive). + * @param to The end of the range to copy (exclusive). + * @return The copied data, or a zero length array if the range is invalid. + */ + private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) { + if (to <= from) { + // Invalid or zero length range. + return new byte[0]; + } + return Arrays.copyOfRange(data, from, to); + } + private static final class Id3Header { private final int majorVersion; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java new file mode 100644 index 0000000000..42ac938677 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -0,0 +1,170 @@ +/* + * 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.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; + +/** + * Abstract base class for the concatenation of one or more {@link Timeline}s. + */ +/* package */ abstract class AbstractConcatenatedTimeline extends Timeline { + + private final int childCount; + + public AbstractConcatenatedTimeline(int childCount) { + this.childCount = childCount; + } + + @Override + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); + if (nextWindowIndexInChild != C.INDEX_UNSET) { + return firstWindowIndexInChild + nextWindowIndexInChild; + } else { + int nextChildIndex = childIndex + 1; + if (nextChildIndex < childCount) { + return getFirstWindowIndexByChildIndex(nextChildIndex); + } else if (repeatMode == Player.REPEAT_MODE_ALL) { + return 0; + } else { + return C.INDEX_UNSET; + } + } + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); + if (previousWindowIndexInChild != C.INDEX_UNSET) { + return firstWindowIndexInChild + previousWindowIndexInChild; + } else { + if (firstWindowIndexInChild > 0) { + return firstWindowIndexInChild - 1; + } else if (repeatMode == Player.REPEAT_MODE_ALL) { + return getWindowCount() - 1; + } else { + return C.INDEX_UNSET; + } + } + } + + @Override + public final Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getWindow(windowIndex - firstWindowIndexInChild, window, + setIds, defaultPositionProjectionUs); + window.firstPeriodIndex += firstPeriodIndexInChild; + window.lastPeriodIndex += firstPeriodIndexInChild; + return window; + } + + @Override + public final Period getPeriod(int periodIndex, Period period, boolean setIds) { + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, + setIds); + period.windowIndex += firstWindowIndexInChild; + if (setIds) { + period.uid = Pair.create(getChildUidByChildIndex(childIndex), period.uid); + } + return period; + } + + @Override + public final int getIndexOfPeriod(Object uid) { + if (!(uid instanceof Pair)) { + return C.INDEX_UNSET; + } + Pair childUidAndPeriodUid = (Pair) uid; + Object childUid = childUidAndPeriodUid.first; + Object periodUid = childUidAndPeriodUid.second; + int childIndex = getChildIndexByChildUid(childUid); + if (childIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); + return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET + : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; + } + + /** + * Returns the index of the child timeline containing the given period index. + * + * @param periodIndex A valid period index within the bounds of the timeline. + */ + protected abstract int getChildIndexByPeriodIndex(int periodIndex); + + /** + * Returns the index of the child timeline containing the given window index. + * + * @param windowIndex A valid window index within the bounds of the timeline. + */ + protected abstract int getChildIndexByWindowIndex(int windowIndex); + + /** + * Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not + * found. + * + * @param childUid A child UID. + * @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found. + */ + protected abstract int getChildIndexByChildUid(Object childUid); + + /** + * Returns the child timeline for the child with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Timeline getTimelineByChildIndex(int childIndex); + + /** + * Returns the first period index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstPeriodIndexByChildIndex(int childIndex); + + /** + * Returns the first window index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstWindowIndexByChildIndex(int childIndex); + + /** + * Returns the UID of the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Object getChildUidByChildIndex(int childIndex); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index e14930c7b8..12f58d9a21 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -16,10 +16,12 @@ package com.google.android.exoplayer2.source; 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.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; /** @@ -45,14 +47,20 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb *

* The clipping start/end positions must be specified by calling {@link #setClipping(long, long)} * on the playback thread before preparation completes. + *

+ * If the start point is guaranteed to be a key frame, pass {@code false} to {@code + * enableInitialPositionDiscontinuity} to suppress an initial discontinuity when the period is + * first read from. * * @param mediaPeriod The media period to clip. + * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. */ - public ClippingMediaPeriod(MediaPeriod mediaPeriod) { + public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) { this.mediaPeriod = mediaPeriod; startUs = C.TIME_UNSET; endUs = C.TIME_UNSET; sampleStreams = new ClippingSampleStream[0]; + pendingInitialDiscontinuity = enableInitialDiscontinuity; } /** @@ -68,9 +76,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void prepare(MediaPeriod.Callback callback) { + public void prepare(MediaPeriod.Callback callback, long positionUs) { this.callback = callback; - mediaPeriod.prepare(this); + mediaPeriod.prepare(this, startUs + positionUs); } @Override @@ -94,6 +102,9 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, internalStreams, streamResetFlags, positionUs + startUs); + if (pendingInitialDiscontinuity) { + pendingInitialDiscontinuity = startUs != 0 && shouldKeepInitialDiscontinuity(selections); + } Assertions.checkState(enablePositionUs == positionUs + startUs || (enablePositionUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); @@ -179,6 +190,15 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void onPrepared(MediaPeriod mediaPeriod) { Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); + callback.onPrepared(this); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + private static boolean shouldKeepInitialDiscontinuity(TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer // timestamps can be negative, because sample streams provide buffers starting at a key-frame, @@ -186,13 +206,17 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp // read in the previous period. Renderer implementations may not allow this, so we signal a // discontinuity which resets the renderers before they read the clipping sample stream. - pendingInitialDiscontinuity = startUs != 0; - callback.onPrepared(this); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + // However, for audio-only track selections we assume to have random access seek behaviour and + // do not need an initial discontinuity to reset the renderer. + for (TrackSelection trackSelection : selections) { + if (trackSelection != null) { + Format selectedFormat = trackSelection.getSelectedFormat(); + if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) { + return true; + } + } + } + return false; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index be15a07726..32c4eb6c73 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -33,6 +34,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste private final MediaSource mediaSource; private final long startUs; private final long endUs; + private final boolean enableInitialDiscontinuity; private final ArrayList mediaPeriods; private MediaSource.Listener sourceListener; @@ -49,10 +51,31 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * from the specified start point up to the end of the source. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { + this(mediaSource, startPositionUs, endPositionUs, true); + } + + /** + * Creates a new clipping source that wraps the specified source. + *

+ * If the start point is guaranteed to be a key frame, pass {@code false} to + * {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period + * is first read from. + * + * @param mediaSource The single-period, non-dynamic source to wrap. + * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to + * start providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop + * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples + * from the specified start point up to the end of the source. + * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. + */ + public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs, + boolean enableInitialDiscontinuity) { Assertions.checkArgument(startPositionUs >= 0); this.mediaSource = Assertions.checkNotNull(mediaSource); startUs = startPositionUs; endUs = endPositionUs; + this.enableInitialDiscontinuity = enableInitialDiscontinuity; mediaPeriods = new ArrayList<>(); } @@ -68,9 +91,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( - mediaSource.createPeriod(index, allocator, startUs + positionUs)); + mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity); mediaPeriods.add(mediaPeriod); mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); return mediaPeriod; @@ -142,6 +165,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste return 1; } + @Override + public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) { + return timeline.getNextWindowIndex(windowIndex, repeatMode); + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) { + return timeline.getPreviousWindowIndex(windowIndex, repeatMode); + } + @Override public Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index c28a016581..343d4f0bbe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -29,7 +29,19 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { } @Override - public long getNextLoadPositionUs() { + public final long getBufferedPositionUs() { + long bufferedPositionUs = Long.MAX_VALUE; + for (SequenceableLoader loader : loaders) { + long loaderBufferedPositionUs = loader.getBufferedPositionUs(); + if (loaderBufferedPositionUs != C.TIME_END_OF_SOURCE) { + bufferedPositionUs = Math.min(bufferedPositionUs, loaderBufferedPositionUs); + } + } + return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + } + + @Override + public final long getNextLoadPositionUs() { long nextLoadPositionUs = Long.MAX_VALUE; for (SequenceableLoader loader : loaders) { long loaderNextLoadPositionUs = loader.getNextLoadPositionUs(); @@ -41,7 +53,7 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { } @Override - public boolean continueLoading(long positionUs) { + public final boolean continueLoading(long positionUs) { boolean madeProgress = false; boolean madeProgressThisIteration; do { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 9fc499f251..5d2bbcc33e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.source; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -38,6 +38,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final Object[] manifests; private final Map sourceIndexByMediaPeriod; private final boolean[] duplicateFlags; + private final boolean isRepeatOneAtomic; private Listener listener; private ConcatenatedTimeline timeline; @@ -47,7 +48,21 @@ public final class ConcatenatingMediaSource implements MediaSource { * {@link MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(MediaSource... mediaSources) { + this(false, mediaSources); + } + + /** + * @param isRepeatOneAtomic Whether the concatenated media source shall be treated as atomic + * (i.e., repeated in its entirety) when repeat mode is set to {@code Player.REPEAT_MODE_ONE}. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same + * {@link MediaSource} instance to be present more than once in the array. + */ + public ConcatenatingMediaSource(boolean isRepeatOneAtomic, MediaSource... mediaSources) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } this.mediaSources = mediaSources; + this.isRepeatOneAtomic = isRepeatOneAtomic; timelines = new Timeline[mediaSources.length]; manifests = new Object[mediaSources.length]; sourceIndexByMediaPeriod = new HashMap<>(); @@ -80,11 +95,11 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - int sourceIndex = timeline.getSourceIndexForPeriod(index); - int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex); - MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator, - positionUs); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); + MediaPeriodId periodIdInSource = + new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); + MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); return mediaPeriod; } @@ -123,7 +138,7 @@ public final class ConcatenatingMediaSource implements MediaSource { return; } } - timeline = new ConcatenatedTimeline(timelines.clone()); + timeline = new ConcatenatedTimeline(timelines.clone(), isRepeatOneAtomic); listener.onSourceInfoRefreshed(timeline, manifests.clone()); } @@ -144,13 +159,15 @@ public final class ConcatenatingMediaSource implements MediaSource { /** * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s. */ - private static final class ConcatenatedTimeline extends Timeline { + private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { private final Timeline[] timelines; private final int[] sourcePeriodOffsets; private final int[] sourceWindowOffsets; + private final boolean isRepeatOneAtomic; - public ConcatenatedTimeline(Timeline[] timelines) { + public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { + super(timelines.length); int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; @@ -167,6 +184,7 @@ public final class ConcatenatingMediaSource implements MediaSource { this.timelines = timelines; this.sourcePeriodOffsets = sourcePeriodOffsets; this.sourceWindowOffsets = sourceWindowOffsets; + this.isRepeatOneAtomic = isRepeatOneAtomic; } @Override @@ -174,70 +192,63 @@ public final class ConcatenatingMediaSource implements MediaSource { return sourceWindowOffsets[sourceWindowOffsets.length - 1]; } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - int sourceIndex = getSourceIndexForWindow(windowIndex); - int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); - int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds, - defaultPositionProjectionUs); - window.firstPeriodIndex += firstPeriodIndexInSource; - window.lastPeriodIndex += firstPeriodIndexInSource; - return window; - } - @Override public int getPeriodCount() { return sourcePeriodOffsets[sourcePeriodOffsets.length - 1]; } @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - int sourceIndex = getSourceIndexForPeriod(periodIndex); - int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex); - int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex); - timelines[sourceIndex].getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds); - period.windowIndex += firstWindowIndexInSource; - if (setIds) { - period.uid = Pair.create(sourceIndex, period.uid); + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + repeatMode = Player.REPEAT_MODE_ALL; } - return period; + return super.getNextWindowIndex(windowIndex, repeatMode); } @Override - public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Pair)) { - return C.INDEX_UNSET; + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + repeatMode = Player.REPEAT_MODE_ALL; } - Pair sourceIndexAndPeriodId = (Pair) uid; - if (!(sourceIndexAndPeriodId.first instanceof Integer)) { - return C.INDEX_UNSET; - } - int sourceIndex = (Integer) sourceIndexAndPeriodId.first; - Object periodId = sourceIndexAndPeriodId.second; - if (sourceIndex < 0 || sourceIndex >= timelines.length) { - return C.INDEX_UNSET; - } - int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(periodId); - return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET - : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource; + return super.getPreviousWindowIndex(windowIndex, repeatMode); } - private int getSourceIndexForPeriod(int periodIndex) { + @Override + protected int getChildIndexByPeriodIndex(int periodIndex) { return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; } - private int getFirstPeriodIndexInSource(int sourceIndex) { - return sourceIndex == 0 ? 0 : sourcePeriodOffsets[sourceIndex - 1]; - } - - private int getSourceIndexForWindow(int windowIndex) { + @Override + protected int getChildIndexByWindowIndex(int windowIndex) { return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; } - private int getFirstWindowIndexInSource(int sourceIndex) { - return sourceIndex == 0 ? 0 : sourceWindowOffsets[sourceIndex - 1]; + @Override + protected int getChildIndexByChildUid(Object childUid) { + if (!(childUid instanceof Integer)) { + return C.INDEX_UNSET; + } + return (Integer) childUid; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1]; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1]; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return childIndex; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java new file mode 100644 index 0000000000..b00732e839 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -0,0 +1,639 @@ +/* + * 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.util.Pair; +import android.util.SparseIntArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** + * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified + * during playback. Access to this class is thread-safe. + */ +public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { + + private static final int MSG_ADD = 0; + private static final int MSG_ADD_MULTIPLE = 1; + private static final int MSG_REMOVE = 2; + private static final int MSG_MOVE = 3; + + // Accessed on the app thread. + private final List mediaSourcesPublic; + + // Accessed on the playback thread. + private final List mediaSourceHolders; + private final MediaSourceHolder query; + private final Map mediaSourceByMediaPeriod; + private final List deferredMediaPeriods; + + private ExoPlayer player; + private Listener listener; + private boolean preventListenerNotification; + private int windowCount; + private int periodCount; + + public DynamicConcatenatingMediaSource() { + this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); + this.mediaSourcesPublic = new ArrayList<>(); + this.mediaSourceHolders = new ArrayList<>(); + this.deferredMediaPeriods = new ArrayList<>(1); + this.query = new MediaSourceHolder(null, null, -1, -1, -1); + } + + /** + * Appends a {@link MediaSource} to the playlist. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public synchronized void addMediaSource(MediaSource mediaSource) { + addMediaSource(mediaSourcesPublic.size(), mediaSource); + } + + /** + * Adds a {@link MediaSource} to the playlist. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public synchronized void addMediaSource(int index, MediaSource mediaSource) { + Assertions.checkNotNull(mediaSource); + Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); + mediaSourcesPublic.add(index, mediaSource); + if (player != null) { + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, Pair.create(index, mediaSource))); + } + } + + /** + * Appends multiple {@link MediaSource}s to the playlist. + * + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public synchronized void addMediaSources(Collection mediaSources) { + addMediaSources(mediaSourcesPublic.size(), mediaSources); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media + * sources are added in the order in which they appear in this collection. + */ + public synchronized void addMediaSources(int index, Collection mediaSources) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); + } + mediaSourcesPublic.addAll(index, mediaSources); + if (player != null && !mediaSources.isEmpty()) { + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, + Pair.create(index, mediaSources))); + } + } + + /** + * Removes a {@link MediaSource} from the playlist. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + */ + public synchronized void removeMediaSource(int index) { + mediaSourcesPublic.remove(index); + if (player != null) { + player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, index)); + } + } + + /** + * Moves an existing {@link MediaSource} within the playlist. + * + * @param currentIndex The current index of the media source in the playlist. This index must be + * in the range of 0 <= index < {@link #getSize()}. + * @param newIndex The target index of the media source in the playlist. This index must be in the + * range of 0 <= index < {@link #getSize()}. + */ + public synchronized void moveMediaSource(int currentIndex, int newIndex) { + if (currentIndex == newIndex) { + return; + } + mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); + if (player != null) { + player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, + Pair.create(currentIndex, newIndex))); + } + } + + /** + * Returns the number of media sources in the playlist. + */ + public synchronized int getSize() { + return mediaSourcesPublic.size(); + } + + /** + * Returns the {@link MediaSource} at a specified index. + * + * @param index A index in the range of 0 <= index <= {@link #getSize()}. + * @return The {@link MediaSource} at this index. + */ + public synchronized MediaSource getMediaSource(int index) { + return mediaSourcesPublic.get(index); + } + + @Override + public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource, + Listener listener) { + this.player = player; + this.listener = listener; + preventListenerNotification = true; + addMediaSourcesInternal(0, mediaSourcesPublic); + preventListenerNotification = false; + maybeNotifyListener(); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); + MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); + MediaPeriodId idInSource = new MediaPeriodId(id.periodIndex - holder.firstPeriodIndexInChild); + MediaPeriod mediaPeriod; + if (!holder.isPrepared) { + mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); + deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod); + } else { + mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator); + } + mediaSourceByMediaPeriod.put(mediaPeriod, holder.mediaSource); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + MediaSource mediaSource = mediaSourceByMediaPeriod.get(mediaPeriod); + mediaSourceByMediaPeriod.remove(mediaPeriod); + if (mediaPeriod instanceof DeferredMediaPeriod) { + deferredMediaPeriods.remove(mediaPeriod); + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); + } else { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void releaseSource() { + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + mediaSourceHolder.mediaSource.releaseSource(); + } + } + + @Override + @SuppressWarnings("unchecked") + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + preventListenerNotification = true; + switch (messageType) { + case MSG_ADD: { + Pair messageData = (Pair) message; + addMediaSourceInternal(messageData.first, messageData.second); + break; + } + case MSG_ADD_MULTIPLE: { + Pair> messageData = + (Pair>) message; + addMediaSourcesInternal(messageData.first, messageData.second); + break; + } + case MSG_REMOVE: { + removeMediaSourceInternal((Integer) message); + break; + } + case MSG_MOVE: { + Pair messageData = (Pair) message; + moveMediaSourceInternal(messageData.first, messageData.second); + break; + } + default: { + throw new IllegalStateException(); + } + } + preventListenerNotification = false; + maybeNotifyListener(); + } + + private void maybeNotifyListener() { + if (!preventListenerNotification) { + listener.onSourceInfoRefreshed( + new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount), null); + } + } + + private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) { + final MediaSourceHolder newMediaSourceHolder; + Object newUid = System.identityHashCode(newMediaSource); + DeferredTimeline newTimeline = new DeferredTimeline(); + if (newIndex > 0) { + MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, + previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), + previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount(), + newUid); + } else { + newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, newUid); + } + correctOffsets(newIndex, newTimeline.getWindowCount(), newTimeline.getPeriodCount()); + mediaSourceHolders.add(newIndex, newMediaSourceHolder); + newMediaSourceHolder.mediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) { + updateMediaSourceInternal(newMediaSourceHolder, newTimeline); + } + }); + } + + private void addMediaSourcesInternal(int index, Collection mediaSources) { + for (MediaSource mediaSource : mediaSources) { + addMediaSourceInternal(index++, mediaSource); + } + } + + private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { + if (mediaSourceHolder == null) { + throw new IllegalArgumentException(); + } + DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; + if (deferredTimeline.getTimeline() == timeline) { + return; + } + int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount(); + int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount(); + if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) { + int index = findMediaSourceHolderByPeriodIndex(mediaSourceHolder.firstPeriodIndexInChild); + correctOffsets(index + 1, windowOffsetUpdate, periodOffsetUpdate); + } + mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline); + if (!mediaSourceHolder.isPrepared) { + for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) { + if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) { + deferredMediaPeriods.get(i).createPeriod(); + deferredMediaPeriods.remove(i); + } + } + } + mediaSourceHolder.isPrepared = true; + maybeNotifyListener(); + } + + private void removeMediaSourceInternal(int index) { + MediaSourceHolder holder = mediaSourceHolders.get(index); + mediaSourceHolders.remove(index); + Timeline oldTimeline = holder.timeline; + correctOffsets(index, -oldTimeline.getWindowCount(), -oldTimeline.getPeriodCount()); + holder.mediaSource.releaseSource(); + } + + private void moveMediaSourceInternal(int currentIndex, int newIndex) { + int startIndex = Math.min(currentIndex, newIndex); + int endIndex = Math.max(currentIndex, newIndex); + int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; + int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild; + mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex)); + for (int i = startIndex; i <= endIndex; i++) { + MediaSourceHolder holder = mediaSourceHolders.get(i); + holder.firstWindowIndexInChild = windowOffset; + holder.firstPeriodIndexInChild = periodOffset; + windowOffset += holder.timeline.getWindowCount(); + periodOffset += holder.timeline.getPeriodCount(); + } + } + + private void correctOffsets(int startIndex, int windowOffsetUpdate, int periodOffsetUpdate) { + windowCount += windowOffsetUpdate; + periodCount += periodOffsetUpdate; + for (int i = startIndex; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate; + mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate; + } + } + + private int findMediaSourceHolderByPeriodIndex(int periodIndex) { + query.firstPeriodIndexInChild = periodIndex; + int index = Collections.binarySearch(mediaSourceHolders, query); + return index >= 0 ? index : -index - 2; + } + + private static final class MediaSourceHolder implements Comparable { + + public final MediaSource mediaSource; + public final Object uid; + + public DeferredTimeline timeline; + public int firstWindowIndexInChild; + public int firstPeriodIndexInChild; + public boolean isPrepared; + + public MediaSourceHolder(MediaSource mediaSource, DeferredTimeline timeline, int window, + int period, Object uid) { + this.mediaSource = mediaSource; + this.timeline = timeline; + this.firstWindowIndexInChild = window; + this.firstPeriodIndexInChild = period; + this.uid = uid; + } + + @Override + public int compareTo(MediaSourceHolder other) { + return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; + } + } + + private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { + + private final int windowCount; + private final int periodCount; + private final int[] firstPeriodInChildIndices; + private final int[] firstWindowInChildIndices; + private final Timeline[] timelines; + private final int[] uids; + private final SparseIntArray childIndexByUid; + + public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, + int periodCount) { + super(mediaSourceHolders.size()); + this.windowCount = windowCount; + this.periodCount = periodCount; + int childCount = mediaSourceHolders.size(); + firstPeriodInChildIndices = new int[childCount]; + firstWindowInChildIndices = new int[childCount]; + timelines = new Timeline[childCount]; + uids = new int[childCount]; + childIndexByUid = new SparseIntArray(); + int index = 0; + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + timelines[index] = mediaSourceHolder.timeline; + firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; + firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; + uids[index] = (int) mediaSourceHolder.uid; + childIndexByUid.put(uids[index], index++); + } + } + + @Override + protected int getChildIndexByPeriodIndex(int periodIndex) { + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); + } + + @Override + protected int getChildIndexByWindowIndex(int windowIndex) { + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); + } + + @Override + protected int getChildIndexByChildUid(Object childUid) { + if (!(childUid instanceof Integer)) { + return C.INDEX_UNSET; + } + int index = childIndexByUid.get((int) childUid, -1); + return index == -1 ? C.INDEX_UNSET : index; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return firstPeriodInChildIndices[childIndex]; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return firstWindowInChildIndices[childIndex]; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return uids[childIndex]; + } + + @Override + public int getWindowCount() { + return windowCount; + } + + @Override + public int getPeriodCount() { + return periodCount; + } + + } + + private static final class DeferredTimeline extends Timeline { + + private static final Object DUMMY_ID = new Object(); + private static final Period period = new Period(); + + private final Timeline timeline; + private final Object replacedID; + + public DeferredTimeline() { + timeline = null; + replacedID = null; + } + + private DeferredTimeline(Timeline timeline, Object replacedID) { + this.timeline = timeline; + this.replacedID = replacedID; + } + + public DeferredTimeline cloneWithNewTimeline(Timeline timeline) { + return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0 + ? timeline.getPeriod(0, period, true).uid : replacedID); + } + + public Timeline getTimeline() { + return timeline; + } + + @Override + public int getWindowCount() { + return timeline == null ? 1 : timeline.getWindowCount(); + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return timeline == null + // Dynamic window to indicate pending timeline updates. + ? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0, + C.TIME_UNSET, 0, 0, 0) + : timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return timeline == null ? 1 : timeline.getPeriodCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + if (timeline == null) { + return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET, + C.TIME_UNSET); + } + timeline.getPeriod(periodIndex, period, setIds); + if (period.uid == replacedID) { + period.uid = DUMMY_ID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET) + : timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid); + } + + } + + private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaSource mediaSource; + + private final MediaPeriodId id; + private final Allocator allocator; + + private MediaPeriod mediaPeriod; + private Callback callback; + private long preparePositionUs; + + public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + } + + public void createPeriod() { + mediaPeriod = mediaSource.createPeriod(id, allocator); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + this.preparePositionUs = preparePositionUs; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + callback.onPrepared(this); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index f247d4dd37..e7273f834b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -17,20 +17,18 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; -import android.util.SparseArray; 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.extractor.DefaultExtractorInput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -43,12 +41,29 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.util.Arrays; /** * A {@link MediaPeriod} that extracts data using an {@link Extractor}. */ /* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput, - Loader.Callback, UpstreamFormatChangedListener { + Loader.Callback, Loader.ReleaseCallback, + UpstreamFormatChangedListener { + + /** + * Listener for information about the period. + */ + interface Listener { + + /** + * Called when the duration or ability to seek within the period changes. + * + * @param durationUs The duration of the period, or {@link C#TIME_UNSET}. + * @param isSeekable Whether the period is seekable. + */ + void onSourceInfoRefreshed(long durationUs, boolean isSeekable); + + } /** * When the source's duration is unknown, it is calculated by adding this value to the largest @@ -61,24 +76,26 @@ import java.io.IOException; private final int minLoadableRetryCount; private final Handler eventHandler; private final ExtractorMediaSource.EventListener eventListener; - private final MediaSource.Listener sourceListener; + private final Listener listener; private final Allocator allocator; private final String customCacheKey; + private final long continueLoadingCheckIntervalBytes; private final Loader loader; private final ExtractorHolder extractorHolder; private final ConditionVariable loadCondition; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; - private final SparseArray sampleQueues; private Callback callback; private SeekMap seekMap; - private boolean tracksBuilt; + private SampleQueue[] sampleQueues; + private int[] sampleQueueTrackIds; + private boolean sampleQueuesBuilt; private boolean prepared; private boolean seenFirstTrackSelection; - private boolean notifyReset; + private boolean notifyDiscontinuity; private int enabledTrackCount; private TrackGroupArray tracks; private long durationUs; @@ -101,23 +118,26 @@ import java.io.IOException; * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param sourceListener A listener to notify when the timeline has been loaded. + * @param listener A listener to notify when information about the period changes. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each + * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. */ public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, int minLoadableRetryCount, Handler eventHandler, - ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener, - Allocator allocator, String customCacheKey) { + ExtractorMediaSource.EventListener eventListener, Listener listener, + Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; this.minLoadableRetryCount = minLoadableRetryCount; this.eventHandler = eventHandler; this.eventListener = eventListener; - this.sourceListener = sourceListener; + this.listener = listener; this.allocator = allocator; this.customCacheKey = customCacheKey; + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; loader = new Loader("Loader:ExtractorMediaPeriod"); extractorHolder = new ExtractorHolder(extractors, this); loadCondition = new ConditionVariable(); @@ -136,30 +156,35 @@ import java.io.IOException; } }; handler = new Handler(); - + sampleQueueTrackIds = new int[0]; + sampleQueues = new SampleQueue[0]; pendingResetPositionUs = C.TIME_UNSET; - sampleQueues = new SparseArray<>(); length = C.LENGTH_UNSET; } public void release() { - final ExtractorHolder extractorHolder = this.extractorHolder; - loader.release(new Runnable() { - @Override - public void run() { - extractorHolder.release(); - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).disable(); - } + boolean releasedSynchronously = loader.release(this); + if (prepared && !releasedSynchronously) { + // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise + // sampleQueues may still be being modified by the loading thread. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); } - }); + } handler.removeCallbacksAndMessages(null); released = true; } @Override - public void prepare(Callback callback) { + public void onLoaderReleased() { + extractorHolder.release(); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + } + + @Override + public void prepare(Callback callback, long positionUs) { this.callback = callback; loadCondition.open(); startLoading(); @@ -179,19 +204,21 @@ import java.io.IOException; public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { Assertions.checkState(prepared); - // Disable old tracks. + int oldEnabledTrackCount = enabledTrackCount; + // Deselect old tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { int track = ((SampleStreamImpl) streams[i]).track; Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; trackEnabledStates[track] = false; - sampleQueues.valueAt(track).disable(); streams[i] = null; } } - // Enable new tracks. - boolean selectedNewTracks = false; + // We'll always need to seek if this is a first selection to a non-zero position, or if we're + // making a selection having previously disabled all tracks. + boolean seekRequired = seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != 0; + // Select new tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; @@ -203,25 +230,33 @@ import java.io.IOException; trackEnabledStates[track] = true; streams[i] = new SampleStreamImpl(track); streamResetFlags[i] = true; - selectedNewTracks = true; - } - } - if (!seenFirstTrackSelection) { - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - if (!trackEnabledStates[i]) { - sampleQueues.valueAt(i).disable(); + // If there's still a chance of avoiding a seek, try and seek within the sample queue. + if (!seekRequired) { + SampleQueue sampleQueue = sampleQueues[track]; + sampleQueue.rewind(); + // A seek can be avoided if we're able to advance to the current playback position in the + // sample queue, or if we haven't read anything from the queue since the previous seek + // (this case is common for sparse tracks such as metadata tracks). In all other cases a + // seek is required. + seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + && sampleQueue.getReadIndex() != 0; } } } if (enabledTrackCount == 0) { - notifyReset = false; + notifyDiscontinuity = false; if (loader.isLoading()) { + // Discard as much as we can synchronously. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } loader.cancelLoading(); + } else { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } } - } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) { + } else if (seekRequired) { positionUs = seekToUs(positionUs); // We'll need to reset renderers consuming from all streams due to the seek. for (int i = 0; i < streams.length; i++) { @@ -236,7 +271,10 @@ import java.io.IOException; @Override public void discardBuffer(long positionUs) { - // Do nothing. + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + sampleQueues[i].discardTo(positionUs, false, trackEnabledStates[i]); + } } @Override @@ -259,8 +297,8 @@ import java.io.IOException; @Override public long readDiscontinuity() { - if (notifyReset) { - notifyReset = false; + if (notifyDiscontinuity) { + notifyDiscontinuity = false; return lastSeekPositionUs; } return C.TIME_UNSET; @@ -277,11 +315,11 @@ import java.io.IOException; if (haveAudioVideoTracks) { // Ignore non-AV tracks, which may be sparse or poorly interleaved. largestQueuedTimestampUs = Long.MAX_VALUE; - int trackCount = sampleQueues.size(); + int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { if (trackIsAudioVideoFlags[i]) { largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + sampleQueues[i].getLargestQueuedTimestampUs()); } } } else { @@ -296,34 +334,28 @@ import java.io.IOException; // Treat all seeks into non-seekable media as being to t=0. positionUs = seekMap.isSeekable() ? positionUs : 0; lastSeekPositionUs = positionUs; - int trackCount = sampleQueues.size(); - // If we're not pending a reset, see if we can seek within the sample queues. - boolean seekInsideBuffer = !isPendingReset(); - for (int i = 0; seekInsideBuffer && i < trackCount; i++) { - if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs, false); + notifyDiscontinuity = false; + // If we're not pending a reset, see if we can seek within the buffer. + if (!isPendingReset() && seekInsideBufferUs(positionUs)) { + return positionUs; + } + // We were unable to seek within the buffer, so need to reset. + pendingResetPositionUs = positionUs; + loadingFinished = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); } } - // If we failed to seek within the sample queues, we need to restart. - if (!seekInsideBuffer) { - pendingResetPositionUs = positionUs; - loadingFinished = false; - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(trackEnabledStates[i]); - } - } - } - notifyReset = false; return positionUs; } // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty()); + return loadingFinished || (!isPendingReset() && sampleQueues[track].hasNextSample()); } /* package */ void maybeThrowError() throws IOException { @@ -332,20 +364,19 @@ import java.io.IOException; /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { - if (notifyReset || isPendingReset()) { + if (notifyDiscontinuity || isPendingReset()) { return C.RESULT_NOTHING_READ; } - - return sampleQueues.valueAt(track).readData(formatHolder, buffer, formatRequired, - loadingFinished, lastSeekPositionUs); + return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished, + lastSeekPositionUs); } /* package */ void skipData(int track, long positionUs) { - DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track); + SampleQueue sampleQueue = sampleQueues[track]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); + sampleQueue.advanceToEnd(); } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + sampleQueue.advanceTo(positionUs, true, true); } } @@ -360,8 +391,7 @@ import java.io.IOException; long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; - sourceListener.onSourceInfoRefreshed( - new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null); + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); } callback.onContinueLoadingRequested(this); } @@ -369,12 +399,14 @@ import java.io.IOException; @Override public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { + if (released) { + return; + } copyLengthFromLoader(loadable); - if (!released && enabledTrackCount > 0) { - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(trackEnabledStates[i]); - } + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + if (enabledTrackCount > 0) { callback.onContinueLoadingRequested(this); } } @@ -398,18 +430,24 @@ import java.io.IOException; @Override public TrackOutput track(int id, int type) { - DefaultTrackOutput trackOutput = sampleQueues.get(id); - if (trackOutput == null) { - trackOutput = new DefaultTrackOutput(allocator); - trackOutput.setUpstreamFormatChangeListener(this); - sampleQueues.put(id, trackOutput); + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + if (sampleQueueTrackIds[i] == id) { + return sampleQueues[i]; + } } + SampleQueue trackOutput = new SampleQueue(allocator); + trackOutput.setUpstreamFormatChangeListener(this); + sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); + sampleQueueTrackIds[trackCount] = id; + sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); + sampleQueues[trackCount] = trackOutput; return trackOutput; } @Override public void endTracks() { - tracksBuilt = true; + sampleQueuesBuilt = true; handler.post(maybeFinishPrepareRunnable); } @@ -429,22 +467,22 @@ import java.io.IOException; // Internal methods. private void maybeFinishPrepare() { - if (released || prepared || seekMap == null || !tracksBuilt) { + if (released || prepared || seekMap == null || !sampleQueuesBuilt) { return; } - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue.getUpstreamFormat() == null) { return; } } loadCondition.close(); + int trackCount = sampleQueues.length; TrackGroup[] trackArray = new TrackGroup[trackCount]; trackIsAudioVideoFlags = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { - Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat(); + Format trackFormat = sampleQueues[i].getUpstreamFormat(); trackArray[i] = new TrackGroup(trackFormat); String mimeType = trackFormat.sampleMimeType; boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType); @@ -453,8 +491,7 @@ import java.io.IOException; } tracks = new TrackGroupArray(trackArray); prepared = true; - sourceListener.onSourceInfoRefreshed( - new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null); + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); callback.onPrepared(this); } @@ -502,30 +539,51 @@ import java.io.IOException; // previous load finished, so it's necessary to load from the start whenever commencing // a new load. lastSeekPositionUs = 0; - notifyReset = prepared; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]); + notifyDiscontinuity = prepared; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); } loadable.setLoadPosition(0, 0); } } + /** + * Attempts to seek to the specified position within the sample queues. + * + * @param positionUs The seek position in microseconds. + * @return Whether the in-buffer seek was successful. + */ + private boolean seekInsideBufferUs(long positionUs) { + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + SampleQueue sampleQueue = sampleQueues[i]; + sampleQueue.rewind(); + boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false); + // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue + // is successful. We ignore whether seeks within non-AV queues are successful in this case, as + // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is + // successful only if the seek into every queue succeeds. + if (!seekInsideQueue && (trackIsAudioVideoFlags[i] || !haveAudioVideoTracks)) { + return false; + } + sampleQueue.discardToRead(); + } + return true; + } + private int getExtractedSamplesCount() { int extractedSamplesCount = 0; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { - extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex(); + for (SampleQueue sampleQueue : sampleQueues) { + extractedSamplesCount += sampleQueue.getWriteIndex(); } return extractedSamplesCount; } private long getLargestQueuedTimestampUs() { long largestQueuedTimestampUs = Long.MIN_VALUE; - int trackCount = sampleQueues.size(); - for (int i = 0; i < trackCount; i++) { + for (SampleQueue sampleQueue : sampleQueues) { largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + sampleQueue.getLargestQueuedTimestampUs()); } return largestQueuedTimestampUs; } @@ -585,12 +643,6 @@ import java.io.IOException; */ /* package */ final class ExtractingLoadable implements Loadable { - /** - * The number of bytes that should be loaded between each each invocation of - * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. - */ - private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; - private final Uri uri; private final DataSource dataSource; private final ExtractorHolder extractorHolder; @@ -650,7 +702,7 @@ import java.io.IOException; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { loadCondition.block(); result = extractor.read(input, positionHolder); - if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) { + if (input.getPosition() > position + continueLoadingCheckIntervalBytes) { position = input.getPosition(); loadCondition.close(); handler.post(onContinueLoadingRequestedRunnable); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index c560616aae..51e9757165 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -39,7 +39,7 @@ import java.io.IOException; *

* Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ -public final class ExtractorMediaSource implements MediaSource, MediaSource.Listener { +public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener { /** * Listener of {@link ExtractorMediaSource} events. @@ -72,6 +72,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List */ public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; + /** + * The default number of bytes that should be loaded between each each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; + private final Uri uri; private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; @@ -80,10 +86,11 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List private final EventListener eventListener; private final Timeline.Period period; private final String customCacheKey; + private final int continueLoadingCheckIntervalBytes; private MediaSource.Listener sourceListener; - private Timeline timeline; - private boolean timelineHasDuration; + private long timelineDurationUs; + private boolean timelineIsSeekable; /** * @param uri The {@link Uri} of the media stream. @@ -96,8 +103,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List */ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { - this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener, null); + this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } /** @@ -115,7 +121,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, String customCacheKey) { this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, - eventListener, customCacheKey); + eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); } /** @@ -129,10 +135,12 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each + * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. */ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener, String customCacheKey) { + EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; @@ -140,14 +148,14 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List this.eventHandler = eventHandler; this.eventListener = eventListener; this.customCacheKey = customCacheKey; + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; period = new Timeline.Period(); } @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { sourceListener = listener; - timeline = new SinglePeriodTimeline(C.TIME_UNSET, false); - listener.onSourceInfoRefreshed(timeline, null); + notifySourceInfoRefreshed(C.TIME_UNSET, false); } @Override @@ -156,11 +164,11 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator, customCacheKey); + this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); } @Override @@ -173,19 +181,27 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List sourceListener = null; } - // MediaSource.Listener implementation. + // ExtractorMediaPeriod.Listener implementation. @Override - public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) { - long newTimelineDurationUs = newTimeline.getPeriod(0, period).getDurationUs(); - boolean newTimelineHasDuration = newTimelineDurationUs != C.TIME_UNSET; - if (timelineHasDuration && !newTimelineHasDuration) { - // Suppress source info changes that would make the duration unknown when it is already known. + public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { + // If we already have the duration from a previous source info refresh, use it. + durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; + if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable + || (timelineDurationUs != C.TIME_UNSET && durationUs == C.TIME_UNSET)) { + // Suppress no-op source info changes. return; } - timeline = newTimeline; - timelineHasDuration = newTimelineHasDuration; - sourceListener.onSourceInfoRefreshed(timeline, null); + notifySourceInfoRefreshed(durationUs, isSeekable); + } + + // Internal methods. + + private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { + timelineDurationUs = durationUs; + timelineIsSeekable = isSeekable; + sourceListener.onSourceInfoRefreshed( + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 8b14c78234..a6e93a92b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -15,36 +15,30 @@ */ package com.google.android.exoplayer2.source; -import android.util.Log; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; /** - * Loops a {@link MediaSource}. + * Loops a {@link MediaSource} a specified number of times. + *

+ * Note: To loop a {@link MediaSource} indefinitely, it is usually better to use + * {@link ExoPlayer#setRepeatMode(int)}. */ public final class LoopingMediaSource implements MediaSource { - /** - * The maximum number of periods that can be exposed by the source. The value of this constant is - * large enough to cause indefinite looping in practice (the total duration of the looping source - * will be approximately five years if the duration of each period is one second). - */ - public static final int MAX_EXPOSED_PERIODS = 157680000; - - private static final String TAG = "LoopingMediaSource"; - private final MediaSource childSource; private final int loopCount; private int childPeriodCount; /** - * Loops the provided source indefinitely. + * Loops the provided source indefinitely. Note that it is usually better to use + * {@link ExoPlayer#setRepeatMode(int)}. * * @param childSource The {@link MediaSource} to loop. */ @@ -56,9 +50,7 @@ public final class LoopingMediaSource implements MediaSource { * Loops the provided source a specified number of times. * * @param childSource The {@link MediaSource} to loop. - * @param loopCount The desired number of loops. Must be strictly positive. The actual number of - * loops will be capped at the maximum that can achieved without causing the number of - * periods exposed by the source to exceed {@link #MAX_EXPOSED_PERIODS}. + * @param loopCount The desired number of loops. Must be strictly positive. */ public LoopingMediaSource(MediaSource childSource, int loopCount) { Assertions.checkArgument(loopCount > 0); @@ -72,7 +64,9 @@ public final class LoopingMediaSource implements MediaSource { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { childPeriodCount = timeline.getPeriodCount(); - listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest); + Timeline loopingTimeline = loopCount != Integer.MAX_VALUE + ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline); + listener.onSourceInfoRefreshed(loopingTimeline, manifest); } }); } @@ -83,8 +77,10 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - return childSource.createPeriod(index % childPeriodCount, allocator, positionUs); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + return loopCount != Integer.MAX_VALUE + ? childSource.createPeriod(new MediaPeriodId(id.periodIndex % childPeriodCount), allocator) + : childSource.createPeriod(id, allocator); } @Override @@ -97,7 +93,7 @@ public final class LoopingMediaSource implements MediaSource { childSource.releaseSource(); } - private static final class LoopingTimeline extends Timeline { + private static final class LoopingTimeline extends AbstractConcatenatedTimeline { private final Timeline childTimeline; private final int childPeriodCount; @@ -105,20 +101,13 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { + super(loopCount); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); - // This is the maximum number of loops that can be performed without exceeding - // MAX_EXPOSED_PERIODS periods. - int maxLoopCount = MAX_EXPOSED_PERIODS / childPeriodCount; - if (loopCount > maxLoopCount) { - if (loopCount != Integer.MAX_VALUE) { - Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount); - } - this.loopCount = maxLoopCount; - } else { - this.loopCount = loopCount; - } + this.loopCount = loopCount; + Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, + "LoopingMediaSource contains too many periods"); } @Override @@ -126,45 +115,96 @@ public final class LoopingMediaSource implements MediaSource { return childWindowCount * loopCount; } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - childTimeline.getWindow(windowIndex % childWindowCount, window, setIds, - defaultPositionProjectionUs); - int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount; - window.firstPeriodIndex += periodIndexOffset; - window.lastPeriodIndex += periodIndexOffset; - return window; - } - @Override public int getPeriodCount() { return childPeriodCount * loopCount; } @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds); - int loopCount = (periodIndex / childPeriodCount); - period.windowIndex += loopCount * childWindowCount; - if (setIds) { - period.uid = Pair.create(loopCount, period.uid); + protected int getChildIndexByPeriodIndex(int periodIndex) { + return periodIndex / childPeriodCount; + } + + @Override + protected int getChildIndexByWindowIndex(int windowIndex) { + return windowIndex / childWindowCount; + } + + @Override + protected int getChildIndexByChildUid(Object childUid) { + if (!(childUid instanceof Integer)) { + return C.INDEX_UNSET; } - return period; + return (Integer) childUid; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return childTimeline; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return childIndex * childPeriodCount; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return childIndex * childWindowCount; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return childIndex; + } + + } + + private static final class InfinitelyLoopingTimeline extends Timeline { + + private final Timeline childTimeline; + + public InfinitelyLoopingTimeline(Timeline childTimeline) { + this.childTimeline = childTimeline; + } + + @Override + public int getWindowCount() { + return childTimeline.getWindowCount(); + } + + @Override + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode); + return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode); + return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 + : childPreviousWindowIndex; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return childTimeline.getPeriodCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return childTimeline.getPeriod(periodIndex, period, setIds); } @Override public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Pair)) { - return C.INDEX_UNSET; - } - Pair loopCountAndChildUid = (Pair) uid; - if (!(loopCountAndChildUid.first instanceof Integer)) { - return C.INDEX_UNSET; - } - int loopCount = (Integer) loopCountAndChildUid.first; - int periodIndexOffset = loopCount * childPeriodCount; - return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; + return childTimeline.getIndexOfPeriod(uid); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index aaf4c89ff7..7a43dd7562 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -55,8 +55,10 @@ public interface MediaPeriod extends SequenceableLoader { * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. + * @param positionUs The position in microseconds relative to the start of the period at which to + * start loading data. */ - void prepare(Callback callback); + void prepare(Callback callback, long positionUs); /** * Throws an error that's preventing the period from becoming prepared. Does nothing if no such @@ -126,16 +128,6 @@ public interface MediaPeriod extends SequenceableLoader { */ long readDiscontinuity(); - /** - * Returns an estimate of the position up to which data is buffered for the enabled tracks. - *

- * This method should only be called when at least one track is selected. - * - * @return An estimate of the absolute position in microseconds up to which data is buffered, or - * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. - */ - long getBufferedPositionUs(); - /** * Attempts to seek to the specified position in microseconds. *

@@ -151,6 +143,17 @@ public interface MediaPeriod extends SequenceableLoader { // SequenceableLoader interface. Overridden to provide more specific documentation. + /** + * Returns an estimate of the position up to which data is buffered for the enabled tracks. + *

+ * This method should only be called when at least one track is selected. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, or + * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. + */ + @Override + long getBufferedPositionUs(); + /** * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. *

@@ -166,9 +169,9 @@ public interface MediaPeriod extends SequenceableLoader { * This method may be called both during and after the period has been prepared. *

* A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the - * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called - * when the period is permitted to continue loading data. A period may do this both during and - * after preparation. + * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be + * called when the period is permitted to continue loading data. A period may do this both during + * and after preparation. * * @param positionUs The current playback position. * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index f013e790f7..790620a80c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; @@ -34,9 +36,100 @@ public interface MediaSource { * Called when manifest and/or timeline has been refreshed. * * @param timeline The source's timeline. - * @param manifest The loaded manifest. + * @param manifest The loaded manifest. May be null. */ - void onSourceInfoRefreshed(Timeline timeline, Object manifest); + void onSourceInfoRefreshed(Timeline timeline, @Nullable Object manifest); + + } + + /** + * Identifier for a {@link MediaPeriod}. + */ + final class MediaPeriodId { + + /** + * Value for unset media period identifiers. + */ + public static final MediaPeriodId UNSET = + new MediaPeriodId(C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET); + + /** + * The timeline period index. + */ + public final int periodIndex; + + /** + * If the media period is in an ad group, the index of the ad group in the period. + * {@link C#INDEX_UNSET} otherwise. + */ + public final int adGroupIndex; + + /** + * If the media period is in an ad group, the index of the ad in its ad group in the period. + * {@link C#INDEX_UNSET} otherwise. + */ + public final int adIndexInAdGroup; + + /** + * Creates a media period identifier for the specified period in the timeline. + * + * @param periodIndex The timeline period index. + */ + public MediaPeriodId(int periodIndex) { + this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET); + } + + /** + * Creates a media period identifier that identifies an ad within an ad group at the specified + * timeline period. + * + * @param periodIndex The index of the timeline period that contains the ad group. + * @param adGroupIndex The index of the ad group. + * @param adIndexInAdGroup The index of the ad in the ad group. + */ + public MediaPeriodId(int periodIndex, int adGroupIndex, int adIndexInAdGroup) { + this.periodIndex = periodIndex; + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + } + + /** + * Returns a copy of this period identifier but with {@code newPeriodIndex} as its period index. + */ + public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) { + return periodIndex == newPeriodIndex ? this + : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup); + } + + /** + * Returns whether this period identifier identifies an ad in an ad group in a period. + */ + public boolean isAd() { + return adGroupIndex != C.INDEX_UNSET; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + MediaPeriodId periodId = (MediaPeriodId) obj; + return periodIndex == periodId.periodIndex && adGroupIndex == periodId.adGroupIndex + && adIndexInAdGroup == periodId.adIndexInAdGroup; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + periodIndex; + result = 31 * result + adGroupIndex; + result = 31 * result + adIndexInAdGroup; + return result; + } } @@ -58,16 +151,15 @@ public interface MediaSource { void maybeThrowSourceInfoRefreshError() throws IOException; /** - * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}. - * This method may be called multiple times with the same index without an intervening call to + * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called + * multiple times with the same period identifier without an intervening call to * {@link #releasePeriod(MediaPeriod)}. * - * @param index The index of the period. + * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. - * @param positionUs The player's current playback position. * @return A new {@link MediaPeriod}. */ - MediaPeriod createPeriod(int index, Allocator allocator, long positionUs); + MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator); /** * Releases the period. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 077b5576c1..e6a4d4e603 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -44,11 +44,11 @@ import java.util.IdentityHashMap; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; pendingChildPrepareCount = periods.length; for (MediaPeriod period : periods) { - period.prepare(this); + period.prepare(this, positionUs); } } @@ -168,14 +168,7 @@ import java.util.IdentityHashMap; @Override public long getBufferedPositionUs() { - long bufferedPositionUs = Long.MAX_VALUE; - for (MediaPeriod period : enabledPeriods) { - long rendererBufferedPositionUs = period.getBufferedPositionUs(); - if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } - return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + return sequenceableLoader.getBufferedPositionUs(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 6f37165916..642752b35b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -116,10 +116,10 @@ public final class MergingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; for (int i = 0; i < periods.length; i++) { - periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs); + periods[i] = mediaSources[i].createPeriod(id, allocator); } return new MergingMediaPeriod(periods); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java new file mode 100644 index 0000000000..03b2e3b715 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -0,0 +1,541 @@ +/* + * 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.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * A queue of metadata describing the contents of a media buffer. + */ +/* package */ final class SampleMetadataQueue { + + /** + * A holder for sample metadata not held by {@link DecoderInputBuffer}. + */ + public static final class SampleExtrasHolder { + + public int size; + public long offset; + public CryptoData cryptoData; + + } + + private static final int SAMPLE_CAPACITY_INCREMENT = 1000; + + private int capacity; + private int[] sourceIds; + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + private CryptoData[] cryptoDatas; + private Format[] formats; + + private int length; + private int absoluteStartIndex; + private int relativeStartIndex; + private int readPosition; + + private long largestDiscardedTimestampUs; + private long largestQueuedTimestampUs; + private boolean upstreamKeyframeRequired; + private boolean upstreamFormatRequired; + private Format upstreamFormat; + private int upstreamSourceId; + + public SampleMetadataQueue() { + capacity = SAMPLE_CAPACITY_INCREMENT; + sourceIds = new int[capacity]; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + cryptoDatas = new CryptoData[capacity]; + formats = new Format[capacity]; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + upstreamFormatRequired = true; + upstreamKeyframeRequired = true; + } + + /** + * Clears all sample metadata from the queue. + * + * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, + * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) + * are assumed to have the current upstream format. If set to true, {@link #format(Format)} + * must be called after the reset before any more samples can be queued. + */ + public void reset(boolean resetUpstreamFormat) { + length = 0; + absoluteStartIndex = 0; + relativeStartIndex = 0; + readPosition = 0; + upstreamKeyframeRequired = true; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + if (resetUpstreamFormat) { + upstreamFormat = null; + upstreamFormatRequired = true; + } + } + + /** + * Returns the current absolute write index. + */ + public int getWriteIndex() { + return absoluteStartIndex + length; + } + + /** + * Discards samples from the write side of the queue. + * + * @param discardFromIndex The absolute index of the first sample to be discarded. + * @return The reduced total number of bytes written after the samples have been discarded, or 0 + * if the queue is now empty. + */ + public long discardUpstreamSamples(int discardFromIndex) { + int discardCount = getWriteIndex() - discardFromIndex; + Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); + length -= discardCount; + largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + if (length == 0) { + return 0; + } else { + int relativeLastWriteIndex = getRelativeIndex(length - 1); + return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; + } + } + + public void sourceId(int sourceId) { + upstreamSourceId = sourceId; + } + + // Called by the consuming thread. + + /** + * Returns the current absolute read index. + */ + public int getReadIndex() { + return absoluteStartIndex + readPosition; + } + + /** + * Peeks the source id of the next sample to be read, or the current upstream source id if the + * queue is empty or if the read position is at the end of the queue. + * + * @return The source id. + */ + public int peekSourceId() { + int relativeReadIndex = getRelativeIndex(readPosition); + return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; + } + + /** + * Returns whether a sample is available to be read. + */ + public synchronized boolean hasNextSample() { + return readPosition != length; + } + + /** + * Returns the upstream {@link Format} in which samples are being queued. + */ + public synchronized Format getUpstreamFormat() { + return upstreamFormatRequired ? null : upstreamFormat; + } + + /** + * Returns the largest sample timestamp that has been queued since the last call to + * {@link #reset(boolean)}. + *

+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. + * + * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no + * samples have been queued. + */ + public synchronized long getLargestQueuedTimestampUs() { + return largestQueuedTimestampUs; + } + + /** + * Rewinds the read position to the first sample retained in the queue. + */ + public synchronized void rewind() { + readPosition = 0; + } + + /** + * Attempts to read from the queue. + * + * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. + * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the + * end of the stream. If a sample is read then the buffer is populated with information + * about the sample, but not its data. The size and absolute position of the data in the + * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present + * and the absolute position of the first byte that may still be required after the current + * sample has been read. May be null if the caller requires that the format of the stream be + * read even if it's not changing. + * @param formatRequired Whether the caller requires that the format of the stream be read even + * if it's not changing. A sample will never be read if set to true, however it is still + * possible for the end of stream or nothing to be read. + * @param loadingFinished True if an empty queue should be considered the end of the stream. + * @param downstreamFormat The current downstream {@link Format}. If the format of the next + * sample is different to the current downstream format then a format will be read. + * @param extrasHolder The holder into which extra sample information should be written. + * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} + * or {@link C#RESULT_BUFFER_READ}. + */ + @SuppressWarnings("ReferenceEquality") + public synchronized int read(FormatHolder formatHolder, DecoderInputBuffer buffer, + boolean formatRequired, boolean loadingFinished, Format downstreamFormat, + SampleExtrasHolder extrasHolder) { + if (!hasNextSample()) { + if (loadingFinished) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null + && (formatRequired || upstreamFormat != downstreamFormat)) { + formatHolder.format = upstreamFormat; + return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + + int relativeReadIndex = getRelativeIndex(readPosition); + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + formatHolder.format = formats[relativeReadIndex]; + return C.RESULT_FORMAT_READ; + } + + if (buffer.isFlagsOnly()) { + return C.RESULT_NOTHING_READ; + } + + buffer.timeUs = timesUs[relativeReadIndex]; + buffer.setFlags(flags[relativeReadIndex]); + extrasHolder.size = sizes[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; + + readPosition++; + return C.RESULT_BUFFER_READ; + } + + /** + * Attempts to advance the read position to the sample before or at the specified time. + * + * @param timeUs The time to advance to. + * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified + * time, rather than to any sample before or at that time. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the queue, by advancing the read position to the last sample (or keyframe) in the + * queue. + * @return Whether the operation was a success. A successful advance is one in which the read + * position was unchanged or advanced, and is now at a sample meeting the specified criteria. + */ + public synchronized boolean advanceTo(long timeUs, boolean toKeyframe, + boolean allowTimeBeyondBuffer) { + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] + || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { + return false; + } + int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); + if (offset == -1) { + return false; + } + readPosition += offset; + return true; + } + + /** + * Advances the read position to the end of the queue. + */ + public synchronized void advanceToEnd() { + if (!hasNextSample()) { + return; + } + readPosition = length; + } + + /** + * Discards up to but not including the sample immediately before or at the specified time. + * + * @param timeUs The time to discard up to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than just any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the read + * position. If false then samples at and beyond the read position may be discarded, in which + * case the read position is advanced to the first remaining sample. + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. + */ + public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + if (length == 0 || timeUs < timesUs[relativeStartIndex]) { + return C.POSITION_UNSET; + } + int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; + int discardCount = findSampleBefore(relativeStartIndex, searchLength, timeUs, toKeyframe); + if (discardCount == -1) { + return C.POSITION_UNSET; + } + return discardSamples(discardCount); + } + + /** + * Discards samples up to but not including the read position. + * + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. + */ + public synchronized long discardToRead() { + if (readPosition == 0) { + return C.POSITION_UNSET; + } + return discardSamples(readPosition); + } + + /** + * Discards all samples in the queue. The read position is also advanced. + * + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. + */ + public synchronized long discardToEnd() { + if (length == 0) { + return C.POSITION_UNSET; + } + return discardSamples(length); + } + + // Called by the loading thread. + + public synchronized boolean format(Format format) { + if (format == null) { + upstreamFormatRequired = true; + return false; + } + upstreamFormatRequired = false; + if (Util.areEqual(format, upstreamFormat)) { + // Suppress changes between equal formats so we can use referential equality in readData. + return false; + } else { + upstreamFormat = format; + return true; + } + } + + public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, + int size, CryptoData cryptoData) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } + Assertions.checkState(!upstreamFormatRequired); + commitSampleTimestamp(timeUs); + + int relativeEndIndex = getRelativeIndex(length); + timesUs[relativeEndIndex] = timeUs; + offsets[relativeEndIndex] = offset; + sizes[relativeEndIndex] = size; + flags[relativeEndIndex] = sampleFlags; + cryptoDatas[relativeEndIndex] = cryptoData; + formats[relativeEndIndex] = upstreamFormat; + sourceIds[relativeEndIndex] = upstreamSourceId; + + length++; + if (length == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + int[] newSourceIds = new int[newCapacity]; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; + Format[] newFormats = new Format[newCapacity]; + int beforeWrap = capacity - relativeStartIndex; + System.arraycopy(offsets, relativeStartIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeStartIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeStartIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeStartIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeStartIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeStartIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeStartIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeStartIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); + System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); + System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + cryptoDatas = newCryptoDatas; + formats = newFormats; + sourceIds = newSourceIds; + relativeStartIndex = 0; + length = capacity; + capacity = newCapacity; + } + } + + public synchronized void commitSampleTimestamp(long timeUs) { + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); + } + + /** + * Attempts to discard samples from the end of the queue to allow samples starting from the + * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. + * + * @param timeUs The timestamp at which the splice occurs. + * @return Whether the splice was successful. + */ + public synchronized boolean attemptSplice(long timeUs) { + if (length == 0) { + return timeUs > largestDiscardedTimestampUs; + } + long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs, + getLargestTimestamp(readPosition)); + if (largestReadTimestampUs >= timeUs) { + return false; + } + int retainCount = length; + int relativeSampleIndex = getRelativeIndex(length - 1); + while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { + retainCount--; + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + discardUpstreamSamples(absoluteStartIndex + retainCount); + return true; + } + + // Internal methods. + + /** + * Finds the sample in the specified range that's before or at the specified time. If + * {@code keyframe} is {@code true} then the sample is additionally required to be a keyframe. + * + * @param relativeStartIndex The relative index from which to start searching. + * @param length The length of the range being searched. + * @param timeUs The specified time. + * @param keyframe Whether only keyframes should be considered. + * @return The offset from {@code relativeStartIndex} to the found sample, or -1 if no matching + * sample was found. + */ + private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { + // This could be optimized to use a binary search, however in practice callers to this method + // normally pass times near to the start of the search region. Hence it's unclear whether + // switching to a binary search would yield any real benefit. + int sampleCountToTarget = -1; + int searchIndex = relativeStartIndex; + for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { + if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + // We've found a suitable sample. + sampleCountToTarget = i; + } + searchIndex++; + if (searchIndex == capacity) { + searchIndex = 0; + } + } + return sampleCountToTarget; + } + + /** + * Discards the specified number of samples. + * + * @param discardCount The number of samples to discard. + * @return The corresponding offset up to which data should be discarded, or + * {@link C#POSITION_UNSET} if no discarding of data is necessary. + */ + private long discardSamples(int discardCount) { + largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, + getLargestTimestamp(discardCount)); + length -= discardCount; + absoluteStartIndex += discardCount; + relativeStartIndex += discardCount; + if (relativeStartIndex >= capacity) { + relativeStartIndex -= capacity; + } + readPosition -= discardCount; + if (readPosition < 0) { + readPosition = 0; + } + if (length == 0) { + int relativeLastDiscardIndex = (relativeStartIndex == 0 ? capacity : relativeStartIndex) - 1; + return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; + } else { + return offsets[relativeStartIndex]; + } + } + + /** + * Finds the largest timestamp of any sample from the start of the queue up to the specified + * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of + * the keyframe itself, and of subsequent frames. + * + * @param length The length of the range being searched. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. + */ + private long getLargestTimestamp(int length) { + if (length == 0) { + return Long.MIN_VALUE; + } + long largestTimestampUs = Long.MIN_VALUE; + int relativeSampleIndex = getRelativeIndex(length - 1); + for (int i = 0; i < length; i++) { + largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); + if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + break; + } + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + return largestTimestampUs; + } + + /** + * Returns the relative index for a given offset from the start of the queue. + * + * @param offset The offset, which must be in the range [0, length]. + */ + private int getRelativeIndex(int offset) { + int relativeIndex = relativeStartIndex + offset; + return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java new file mode 100644 index 0000000000..c7bae8f8b4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -0,0 +1,701 @@ +/* + * 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 android.support.annotation.Nullable; +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.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder; +import com.google.android.exoplayer2.upstream.Allocation; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A queue of media samples. + */ +public final class SampleQueue implements TrackOutput { + + /** + * A listener for changes to the upstream format. + */ + public interface UpstreamFormatChangedListener { + + /** + * Called on the loading thread when an upstream format change occurs. + * + * @param format The new upstream format. + */ + void onUpstreamFormatChanged(Format format); + + } + + private static final int INITIAL_SCRATCH_SIZE = 32; + + private final Allocator allocator; + private final int allocationLength; + private final SampleMetadataQueue metadataQueue; + private final SampleExtrasHolder extrasHolder; + private final ParsableByteArray scratch; + + // References into the linked list of allocations. + private AllocationNode firstAllocationNode; + private AllocationNode readAllocationNode; + private AllocationNode writeAllocationNode; + + // Accessed only by the consuming thread. + private Format downstreamFormat; + + // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private boolean pendingFormatAdjustment; + private Format lastUnadjustedFormat; + private long sampleOffsetUs; + private long totalBytesWritten; + private boolean pendingSplice; + private UpstreamFormatChangedListener upstreamFormatChangeListener; + + /** + * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. + */ + public SampleQueue(Allocator allocator) { + this.allocator = allocator; + allocationLength = allocator.getIndividualAllocationLength(); + metadataQueue = new SampleMetadataQueue(); + extrasHolder = new SampleExtrasHolder(); + scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** + * Resets the output without clearing the upstream format. Equivalent to {@code reset(false)}. + */ + public void reset() { + reset(false); + } + + /** + * Resets the output. + * + * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, + * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) + * are assumed to have the current upstream format. If set to true, {@link #format(Format)} + * must be called after the reset before any more samples can be queued. + */ + public void reset(boolean resetUpstreamFormat) { + metadataQueue.reset(resetUpstreamFormat); + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + totalBytesWritten = 0; + allocator.trim(); + } + + /** + * Sets a source identifier for subsequent samples. + * + * @param sourceId The source identifier. + */ + public void sourceId(int sourceId) { + metadataQueue.sourceId(sourceId); + } + + /** + * Indicates samples that are subsequently queued should be spliced into those already queued. + */ + public void splice() { + pendingSplice = true; + } + + /** + * Returns the current absolute write index. + */ + public int getWriteIndex() { + return metadataQueue.getWriteIndex(); + } + + /** + * Discards samples from the write side of the queue. + * + * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the + * range [{@link #getReadIndex()}, {@link #getWriteIndex()}]. + */ + public void discardUpstreamSamples(int discardFromIndex) { + totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex); + if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } else { + // Find the last node containing at least 1 byte of data that we need to keep. + AllocationNode lastNodeToKeep = firstAllocationNode; + while (totalBytesWritten > lastNodeToKeep.endPosition) { + lastNodeToKeep = lastNodeToKeep.next; + } + // Discard all subsequent nodes. + AllocationNode firstNodeToDiscard = lastNodeToKeep.next; + clearAllocationNodes(firstNodeToDiscard); + // Reset the successor of the last node to be an uninitialized node. + lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); + // Update writeAllocationNode and readAllocationNode as necessary. + writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next + : lastNodeToKeep; + if (readAllocationNode == firstNodeToDiscard) { + readAllocationNode = lastNodeToKeep.next; + } + } + } + + // Called by the consuming thread. + + /** + * Returns whether a sample is available to be read. + */ + public boolean hasNextSample() { + return metadataQueue.hasNextSample(); + } + + /** + * Returns the current absolute read index. + */ + public int getReadIndex() { + return metadataQueue.getReadIndex(); + } + + /** + * Peeks the source id of the next sample to be read, or the current upstream source id if the + * queue is empty or if the read position is at the end of the queue. + * + * @return The source id. + */ + public int peekSourceId() { + return metadataQueue.peekSourceId(); + } + + /** + * Returns the upstream {@link Format} in which samples are being queued. + */ + public Format getUpstreamFormat() { + return metadataQueue.getUpstreamFormat(); + } + + /** + * Returns the largest sample timestamp that has been queued since the last {@link #reset}. + *

+ * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. + * + * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no + * samples have been queued. + */ + public long getLargestQueuedTimestampUs() { + return metadataQueue.getLargestQueuedTimestampUs(); + } + + /** + * Rewinds the read position to the first sample in the queue. + */ + public void rewind() { + metadataQueue.rewind(); + readAllocationNode = firstAllocationNode; + } + + /** + * Discards up to but not including the sample immediately before or at the specified time. + * + * @param timeUs The time to discard to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the + * read position. If false then samples at and beyond the read position may be discarded, in + * which case the read position is advanced to the first remaining sample. + */ + public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition)); + } + + /** + * Discards up to but not including the read position. + */ + public void discardToRead() { + discardDownstreamTo(metadataQueue.discardToRead()); + } + + /** + * Discards to the end of the queue. The read position is also advanced. + */ + public void discardToEnd() { + discardDownstreamTo(metadataQueue.discardToEnd()); + } + + /** + * Advances the read position to the end of the queue. + */ + public void advanceToEnd() { + metadataQueue.advanceToEnd(); + } + + /** + * Attempts to advance the read position to the sample before or at the specified time. + * + * @param timeUs The time to advance to. + * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified + * time, rather than to any sample before or at that time. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the queue, by advancing the read position to the last sample (or keyframe). + * @return Whether the operation was a success. A successful advance is one in which the read + * position was unchanged or advanced, and is now at a sample meeting the specified criteria. + */ + public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { + return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); + } + + /** + * Attempts to read from the queue. + * + * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. + * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the + * end of the stream. If the end of the stream has been reached, the + * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * @param formatRequired Whether the caller requires that the format of the stream be read even if + * it's not changing. A sample will never be read if set to true, however it is still possible + * for the end of stream or nothing to be read. + * @param loadingFinished True if an empty queue should be considered the end of the stream. + * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will + * be set if the buffer's timestamp is less than this value. + * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or + * {@link C#RESULT_BUFFER_READ}. + */ + public int read(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, + boolean loadingFinished, long decodeOnlyUntilUs) { + int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, + downstreamFormat, extrasHolder); + switch (result) { + case C.RESULT_FORMAT_READ: + downstreamFormat = formatHolder.format; + return C.RESULT_FORMAT_READ; + case C.RESULT_BUFFER_READ: + if (!buffer.isEndOfStream()) { + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Write the sample data into the holder. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + } + return C.RESULT_BUFFER_READ; + case C.RESULT_NOTHING_READ: + return C.RESULT_NOTHING_READ; + default: + throw new IllegalStateException(); + } + } + + /** + * Reads encryption data for the current sample. + *

+ * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and + * {@link SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The + * same value is added to {@link SampleExtrasHolder#offset}. + * + * @param buffer The buffer into which the encryption data should be written. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + long offset = extrasHolder.offset; + + // Read the signal byte. + scratch.reset(1); + readData(offset, scratch.data, 1); + offset++; + byte signalByte = scratch.data[0]; + boolean subsampleEncryption = (signalByte & 0x80) != 0; + int ivSize = signalByte & 0x7F; + + // Read the initialization vector. + if (buffer.cryptoInfo.iv == null) { + buffer.cryptoInfo.iv = new byte[16]; + } + readData(offset, buffer.cryptoInfo.iv, ivSize); + offset += ivSize; + + // Read the subsample count, if present. + int subsampleCount; + if (subsampleEncryption) { + scratch.reset(2); + readData(offset, scratch.data, 2); + offset += 2; + subsampleCount = scratch.readUnsignedShort(); + } else { + subsampleCount = 1; + } + + // Write the clear and encrypted subsample sizes. + int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; + if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { + clearDataSizes = new int[subsampleCount]; + } + int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; + if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { + encryptedDataSizes = new int[subsampleCount]; + } + if (subsampleEncryption) { + int subsampleDataLength = 6 * subsampleCount; + scratch.reset(subsampleDataLength); + readData(offset, scratch.data, subsampleDataLength); + offset += subsampleDataLength; + scratch.setPosition(0); + for (int i = 0; i < subsampleCount; i++) { + clearDataSizes[i] = scratch.readUnsignedShort(); + encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); + } + } else { + clearDataSizes[0] = 0; + encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset); + } + + // Populate the cryptoInfo. + CryptoData cryptoData = extrasHolder.cryptoData; + buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, + cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode, + cryptoData.encryptedBlocks, cryptoData.clearBlocks); + + // Adjust the offset and size to take into account the bytes read. + int bytesRead = (int) (offset - extrasHolder.offset); + extrasHolder.offset += bytesRead; + extrasHolder.size -= bytesRead; + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The buffer into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, ByteBuffer target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The array into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, byte[] target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition), + target, length - remaining, toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Advances {@link #readAllocationNode} to the specified absolute position. + * + * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. + */ + private void advanceReadTo(long absolutePosition) { + while (absolutePosition >= readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + + /** + * Advances {@link #firstAllocationNode} to the specified absolute position. + * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind + * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their + * underlying allocations are returned to the allocator. + * + * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced. + * May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op. + */ + private void discardDownstreamTo(long absolutePosition) { + if (absolutePosition == C.POSITION_UNSET) { + return; + } + while (absolutePosition >= firstAllocationNode.endPosition) { + allocator.release(firstAllocationNode.allocation); + firstAllocationNode = firstAllocationNode.clear(); + } + // If we discarded the node referenced by readAllocationNode then we need to advance it to the + // first remaining node. + if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { + readAllocationNode = firstAllocationNode; + } + } + + // Called by the loading thread. + + /** + * Sets a listener to be notified of changes to the upstream format. + * + * @param listener The listener. + */ + public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { + upstreamFormatChangeListener = listener; + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * that are subsequently queued. + * + * @param sampleOffsetUs The timestamp offset in microseconds. + */ + public void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + pendingFormatAdjustment = true; + } + } + + @Override + public void format(Format format) { + Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); + boolean formatChanged = metadataQueue.format(adjustedFormat); + lastUnadjustedFormat = format; + pendingFormatAdjustment = false; + if (upstreamFormatChangeListener != null && formatChanged) { + upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); + } + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + length = preAppend(length); + int bytesAppended = input.read(writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + postAppend(bytesAppended); + return bytesAppended; + } + + @Override + public void sampleData(ParsableByteArray buffer, int length) { + while (length > 0) { + int bytesAppended = preAppend(length); + buffer.readBytes(writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended); + length -= bytesAppended; + postAppend(bytesAppended); + } + } + + @Override + public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, + CryptoData cryptoData) { + if (pendingFormatAdjustment) { + format(lastUnadjustedFormat); + } + if (pendingSplice) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) { + return; + } + pendingSplice = false; + } + timeUs += sampleOffsetUs; + long absoluteOffset = totalBytesWritten - size - offset; + metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); + } + + // Private methods. + + /** + * Clears allocation nodes starting from {@code fromNode}. + * + * @param fromNode The node from which to clear. + */ + private void clearAllocationNodes(AllocationNode fromNode) { + if (!fromNode.wasInitialized) { + return; + } + // Bulk release allocations for performance (it's significantly faster when using + // DefaultAllocator because the allocator's lock only needs to be acquired and released once) + // [Internal: See b/29542039]. + int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0) + + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength); + Allocation[] allocationsToRelease = new Allocation[allocationCount]; + AllocationNode currentNode = fromNode; + for (int i = 0; i < allocationsToRelease.length; i++) { + allocationsToRelease[i] = currentNode.allocation; + currentNode = currentNode.clear(); + } + allocator.release(allocationsToRelease); + } + + /** + * Called before writing sample data to {@link #writeAllocationNode}. May cause + * {@link #writeAllocationNode} to be initialized. + * + * @param length The number of bytes that the caller wishes to write. + * @return The number of bytes that the caller is permitted to write, which may be less than + * {@code length}. + */ + private int preAppend(int length) { + if (!writeAllocationNode.wasInitialized) { + writeAllocationNode.initialize(allocator.allocate(), + new AllocationNode(writeAllocationNode.endPosition, allocationLength)); + } + return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); + } + + /** + * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. + * + * @param length The number of bytes that were written. + */ + private void postAppend(int length) { + totalBytesWritten += length; + if (totalBytesWritten == writeAllocationNode.endPosition) { + writeAllocationNode = writeAllocationNode.next; + } + } + + /** + * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}. + * + * @param format The {@link Format} to adjust. + * @param sampleOffsetUs The offset to apply. + * @return The adjusted {@link Format}. + */ + private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) { + if (format == null) { + return null; + } + if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { + format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs); + } + return format; + } + + /** + * A node in a linked list of {@link Allocation}s held by the output. + */ + private static final class AllocationNode { + + /** + * The absolute position of the start of the data (inclusive). + */ + public final long startPosition; + /** + * The absolute position of the end of the data (exclusive). + */ + public final long endPosition; + /** + * Whether the node has been initialized. Remains true after {@link #clear()}. + */ + public boolean wasInitialized; + /** + * The {@link Allocation}, or {@code null} if the node is not initialized. + */ + @Nullable public Allocation allocation; + /** + * The next {@link AllocationNode} in the list, or {@code null} if the node has not been + * initialized. Remains set after {@link #clear()}. + */ + @Nullable public AllocationNode next; + + /** + * @param startPosition See {@link #startPosition}. + * @param allocationLength The length of the {@link Allocation} with which this node will be + * initialized. + */ + public AllocationNode(long startPosition, int allocationLength) { + this.startPosition = startPosition; + this.endPosition = startPosition + allocationLength; + } + + /** + * Initializes the node. + * + * @param allocation The node's {@link Allocation}. + * @param next The next {@link AllocationNode}. + */ + public void initialize(Allocation allocation, AllocationNode next) { + this.allocation = allocation; + this.next = next; + wasInitialized = true; + } + + /** + * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to + * the specified absolute position. + * + * @param absolutePosition The absolute position. + * @return The corresponding offset into the allocation's data. + */ + public int translateOffset(long absolutePosition) { + return (int) (absolutePosition - startPosition) + allocation.offset; + } + + /** + * Clears {@link #allocation} and {@link #next}. + * + * @return The cleared next {@link AllocationNode}. + */ + public AllocationNode clear() { + allocation = null; + AllocationNode temp = next; + next = null; + return temp; + } + + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index f287153719..26cb9a2666 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -36,6 +36,14 @@ public interface SequenceableLoader { } + /** + * Returns an estimate of the position up to which data is buffered. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, or + * {@link C#TIME_END_OF_SOURCE} if the data is fully buffered. + */ + long getBufferedPositionUs(); + /** * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 447839392e..6f35438444 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -26,6 +26,8 @@ public final class SinglePeriodTimeline extends Timeline { private static final Object ID = new Object(); + private final long presentationStartTimeMs; + private final long windowStartTimeMs; private final long periodDurationUs; private final long windowDurationUs; private final long windowPositionInPeriodUs; @@ -45,8 +47,8 @@ public final class SinglePeriodTimeline extends Timeline { } /** - * Creates a timeline with one period of known duration, and a window of known duration starting - * at a specified position in the period. + * Creates a timeline with one period, and a window of known duration starting at a specified + * position in the period. * * @param periodDurationUs The duration of the period in microseconds. * @param windowDurationUs The duration of the window in microseconds. @@ -60,6 +62,31 @@ public final class SinglePeriodTimeline extends Timeline { public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { + this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, + windowDefaultStartPositionUs, isSeekable, isDynamic); + } + + /** + * Creates a timeline with one period, and a window of known duration starting at a specified + * position in the period. + * + * @param presentationStartTimeMs The start time of the presentation in milliseconds since the + * epoch. + * @param windowStartTimeMs The window's start time in milliseconds since the epoch. + * @param periodDurationUs The duration of the period in microseconds. + * @param windowDurationUs The duration of the window in microseconds. + * @param windowPositionInPeriodUs The position of the start of the window in the period, in + * microseconds. + * @param windowDefaultStartPositionUs The default position relative to the start of the window at + * which to begin playback, in microseconds. + * @param isSeekable Whether seeking is supported within the window. + * @param isDynamic Whether the window may change when the timeline is updated. + */ + public SinglePeriodTimeline(long presentationStartTimeMs, long windowStartTimeMs, + long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, + long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { + this.presentationStartTimeMs = presentationStartTimeMs; + this.windowStartTimeMs = windowStartTimeMs; this.periodDurationUs = periodDurationUs; this.windowDurationUs = windowDurationUs; this.windowPositionInPeriodUs = windowPositionInPeriodUs; @@ -86,7 +113,7 @@ public final class SinglePeriodTimeline extends Timeline { windowDefaultStartPositionUs = C.TIME_UNSET; } } - return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic, + return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, windowDefaultStartPositionUs, windowDurationUs, 0, 0, windowPositionInPeriodUs); } @@ -99,7 +126,7 @@ public final class SinglePeriodTimeline extends Timeline { public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); + return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 8e38588e89..3435c01eeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -79,7 +79,7 @@ import java.util.Arrays; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { callback.onPrepared(this); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index f6ee84a6f4..99bc60d6fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -95,8 +95,8 @@ public final class SingleSampleMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, eventHandler, eventListener, eventSourceId); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 3882a330f9..9531aaf32e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.source.chunk; import android.util.Log; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; /** @@ -29,22 +29,22 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut private static final String TAG = "BaseMediaChunkOutput"; private final int[] trackTypes; - private final DefaultTrackOutput[] trackOutputs; + private final SampleQueue[] sampleQueues; /** * @param trackTypes The track types of the individual track outputs. - * @param trackOutputs The individual track outputs. + * @param sampleQueues The individual sample queues. */ - public BaseMediaChunkOutput(int[] trackTypes, DefaultTrackOutput[] trackOutputs) { + public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { this.trackTypes = trackTypes; - this.trackOutputs = trackOutputs; + this.sampleQueues = sampleQueues; } @Override public TrackOutput track(int id, int type) { for (int i = 0; i < trackTypes.length; i++) { if (type == trackTypes[i]) { - return trackOutputs[i]; + return sampleQueues[i]; } } Log.e(TAG, "Unmatched track of type: " + type); @@ -52,13 +52,13 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut } /** - * Returns the current absolute write indices of the individual track outputs. + * Returns the current absolute write indices of the individual sample queues. */ public int[] getWriteIndices() { - int[] writeIndices = new int[trackOutputs.length]; - for (int i = 0; i < trackOutputs.length; i++) { - if (trackOutputs[i] != null) { - writeIndices[i] = trackOutputs[i].getWriteIndex(); + int[] writeIndices = new int[sampleQueues.length]; + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueues[i] != null) { + writeIndices[i] = sampleQueues[i].getWriteIndex(); } } return writeIndices; @@ -66,12 +66,12 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut /** * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * subsequently written to the track outputs. + * subsequently written to the sample queues. */ public void setSampleOffsetUs(long sampleOffsetUs) { - for (DefaultTrackOutput trackOutput : trackOutputs) { - if (trackOutput != null) { - trackOutput.setSampleOffsetUs(sampleOffsetUs); + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue != null) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 501f4998cf..07d1cce8cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -186,8 +186,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + CryptoData cryptoData) { + trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index c43f3d577a..0fc3d5881e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -19,8 +19,8 @@ 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.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.upstream.Allocator; @@ -36,7 +36,7 @@ import java.util.List; * May also be configured to expose additional embedded {@link SampleStream}s. */ public class ChunkSampleStream implements SampleStream, SequenceableLoader, - Loader.Callback { + Loader.Callback, Loader.ReleaseCallback { private final int primaryTrackType; private final int[] embeddedTrackTypes; @@ -49,8 +49,8 @@ public class ChunkSampleStream implements SampleStream, S private final ChunkHolder nextChunkHolder; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; - private final DefaultTrackOutput primarySampleQueue; - private final DefaultTrackOutput[] embeddedSampleQueues; + private final SampleQueue primarySampleQueue; + private final SampleQueue[] embeddedSampleQueues; private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; @@ -85,19 +85,19 @@ public class ChunkSampleStream implements SampleStream, S readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; - embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount]; + embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; int[] trackTypes = new int[1 + embeddedTrackCount]; - DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; + SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - primarySampleQueue = new DefaultTrackOutput(allocator); + primarySampleQueue = new SampleQueue(allocator); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { - DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); - embeddedSampleQueues[i] = trackOutput; - sampleQueues[i + 1] = trackOutput; + SampleQueue sampleQueue = new SampleQueue(allocator); + embeddedSampleQueues[i] = sampleQueue; + sampleQueues[i + 1] = sampleQueue; trackTypes[i + 1] = embeddedTrackTypes[i]; } @@ -106,17 +106,20 @@ public class ChunkSampleStream implements SampleStream, S lastSeekPositionUs = positionUs; } + // TODO: Generalize this method to also discard from the primary sample queue and stop discarding + // from this queue in readData and skipData. This will cause samples to be kept in the queue until + // they've been rendered, rather than being discarded as soon as they're read by the renderer. + // This will make in-buffer seeks more likely when seeking slightly forward from the current + // position. This change will need handling with care, in particular when considering removal of + // chunks from the front of the mediaChunks list. /** - * Discards buffered media for embedded tracks that are not currently selected, up to the - * specified position. + * Discards buffered media for embedded tracks, up to the specified position. * * @param positionUs The position to discard up to, in microseconds. */ - public void discardUnselectedEmbeddedTracksTo(long positionUs) { + public void discardEmbeddedTracksTo(long positionUs) { for (int i = 0; i < embeddedSampleQueues.length; i++) { - if (!embeddedTracksSelected[i]) { - embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); - } + embeddedSampleQueues[i].discardTo(positionUs, true, embeddedTracksSelected[i]); } } @@ -135,7 +138,8 @@ public class ChunkSampleStream implements SampleStream, S if (embeddedTrackTypes[i] == trackType) { Assertions.checkState(!embeddedTracksSelected[i]); embeddedTracksSelected[i] = true; - embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + embeddedSampleQueues[i].rewind(); + embeddedSampleQueues[i].advanceTo(positionUs, true, true); return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); } } @@ -181,19 +185,15 @@ public class ChunkSampleStream implements SampleStream, S public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; // If we're not pending a reset, see if we can seek within the primary sample queue. - boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.skipToKeyframeBefore( - positionUs, positionUs < getNextLoadPositionUs()); + boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true, + positionUs < getNextLoadPositionUs()); if (seekInsideBuffer) { - // We succeeded. We need to discard any chunks that we've moved past and perform the seek for - // any embedded streams as well. - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primarySampleQueue.getReadIndex()) { - mediaChunks.removeFirst(); - } - // TODO: For this to work correctly, the embedded streams must not discard anything from their - // sample queues beyond the current read position of the primary stream. - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.skipToKeyframeBefore(positionUs, true); + // We succeeded. Discard samples and corresponding chunks prior to the seek position. + discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); + primarySampleQueue.discardToRead(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.rewind(); + embeddedSampleQueue.discardTo(positionUs, true, false); } } else { // We failed, and need to restart. @@ -203,9 +203,9 @@ public class ChunkSampleStream implements SampleStream, S if (loader.isLoading()) { loader.cancelLoading(); } else { - primarySampleQueue.reset(true); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(true); + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); } } } @@ -217,18 +217,29 @@ public class ChunkSampleStream implements SampleStream, S * This method should be called when the stream is no longer required. */ public void release() { - primarySampleQueue.disable(); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.disable(); + boolean releasedSynchronously = loader.release(this); + if (!releasedSynchronously) { + // Discard as much as we can synchronously. + primarySampleQueue.discardToEnd(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.discardToEnd(); + } + } + } + + @Override + public void onLoaderReleased() { + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); } - loader.release(); } // SampleStream implementation. @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !primarySampleQueue.isEmpty()); + return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample()); } @Override @@ -246,17 +257,22 @@ public class ChunkSampleStream implements SampleStream, S return C.RESULT_NOTHING_READ; } discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); - return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + primarySampleQueue.discardToRead(); + } + return result; } @Override public void skipData(long positionUs) { if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { - primarySampleQueue.skipAll(); + primarySampleQueue.advanceToEnd(); } else { - primarySampleQueue.skipToKeyframeBefore(positionUs, true); + primarySampleQueue.advanceTo(positionUs, true, true); } + primarySampleQueue.discardToRead(); } // Loader.Callback implementation. @@ -279,9 +295,9 @@ public class ChunkSampleStream implements SampleStream, S loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); if (!released) { - primarySampleQueue.reset(true); - for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(true); + primarySampleQueue.reset(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(); } callback.onContinueLoadingRequested(this); } @@ -336,6 +352,7 @@ public class ChunkSampleStream implements SampleStream, S nextChunkHolder.clear(); if (endOfStream) { + pendingResetPositionUs = C.TIME_UNSET; loadingFinished = true; return true; } @@ -389,18 +406,20 @@ public class ChunkSampleStream implements SampleStream, S } private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { - mediaChunks.removeFirst(); + if (!mediaChunks.isEmpty()) { + while (mediaChunks.size() > 1 + && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { + mediaChunks.removeFirst(); + } + BaseMediaChunk currentChunk = mediaChunks.getFirst(); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(primaryDownstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - primaryDownstreamTrackFormat = trackFormat; } /** @@ -413,18 +432,18 @@ public class ChunkSampleStream implements SampleStream, S if (mediaChunks.size() <= queueLength) { return false; } - long startTimeUs = 0; + BaseMediaChunk removed; + long startTimeUs; long endTimeUs = mediaChunks.getLast().endTimeUs; - BaseMediaChunk removed = null; - while (mediaChunks.size() > queueLength) { + do { removed = mediaChunks.removeLast(); startTimeUs = removed.startTimeUs; - loadingFinished = false; - } + } while (mediaChunks.size() > queueLength); primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); } + loadingFinished = false; eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); return true; } @@ -436,11 +455,10 @@ public class ChunkSampleStream implements SampleStream, S public final ChunkSampleStream parent; - private final DefaultTrackOutput sampleQueue; + private final SampleQueue sampleQueue; private final int index; - public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue, - int index) { + public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) { this.parent = parent; this.sampleQueue = sampleQueue; this.index = index; @@ -448,15 +466,15 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty()); + return loadingFinished || (!isPendingReset() && sampleQueue.hasNextSample()); } @Override public void skipData(long positionUs) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); + sampleQueue.advanceToEnd(); } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + sampleQueue.advanceTo(positionUs, true, true); } } @@ -471,7 +489,7 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, + return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index cfbefc0c2e..cc39c88fd0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -93,7 +93,7 @@ public class ContainerMediaChunk extends BaseMediaChunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override public final void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded); try { // Create and open the input. ExtractorInput input = new DefaultExtractorInput(dataSource, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index 69474aa150..4acf0b8525 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -72,7 +72,7 @@ public final class InitializationChunk extends Chunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded); try { // Create and open the input. ExtractorInput input = new DefaultExtractorInput(dataSource, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index a008c9cd84..02cf7dfd55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -85,7 +85,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + DataSpec loadDataSpec = dataSpec.subrange(bytesLoaded); try { // Create and open the input. long length = dataSource.open(loadDataSpec); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 795189e1a6..6a9b83a015 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.Cea708Decoder; import com.google.android.exoplayer2.text.dvb.DvbDecoder; +import com.google.android.exoplayer2.text.ssa.SsaDecoder; import com.google.android.exoplayer2.text.subrip.SubripDecoder; import com.google.android.exoplayer2.text.ttml.TtmlDecoder; import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder; @@ -58,6 +59,7 @@ public interface SubtitleDecoderFactory { *

  • WebVTT (MP4) ({@link Mp4WebvttDecoder})
  • *
  • TTML ({@link TtmlDecoder})
  • *
  • SubRip ({@link SubripDecoder})
  • + *
  • SSA/ASS ({@link SsaDecoder})
  • *
  • TX3G ({@link Tx3gDecoder})
  • *
  • Cea608 ({@link Cea608Decoder})
  • *
  • Cea708 ({@link Cea708Decoder})
  • @@ -70,6 +72,7 @@ public interface SubtitleDecoderFactory { public boolean supportsFormat(Format format) { String mimeType = format.sampleMimeType; return MimeTypes.TEXT_VTT.equals(mimeType) + || MimeTypes.TEXT_SSA.equals(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) @@ -85,6 +88,8 @@ public interface SubtitleDecoderFactory { switch (format.sampleMimeType) { case MimeTypes.TEXT_VTT: return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); case MimeTypes.APPLICATION_MP4VTT: return new Mp4WebvttDecoder(); case MimeTypes.APPLICATION_TTML: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 4950549b19..1820d43e75 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -130,7 +130,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { streamFormat = formats[0]; if (decoder != null) { decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java new file mode 100644 index 0000000000..d2f5a67c27 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -0,0 +1,215 @@ +/* + * 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.text.ssa; + +import android.text.TextUtils; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.LongArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A {@link SimpleSubtitleDecoder} for SSA/ASS. + */ +public final class SsaDecoder extends SimpleSubtitleDecoder { + + private static final String TAG = "SsaDecoder"; + + private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile( + "(?:(\\d+):)?(\\d+):(\\d+)(?::|\\.)(\\d+)"); + private static final String FORMAT_LINE_PREFIX = "Format: "; + private static final String DIALOGUE_LINE_PREFIX = "Dialogue: "; + + private final boolean haveInitializationData; + + private int formatKeyCount; + private int formatStartIndex; + private int formatEndIndex; + private int formatTextIndex; + + public SsaDecoder() { + this(null); + } + + /** + * @param initializationData Optional initialization data for the decoder. If not null, the + * initialization data must consist of two byte arrays. The first must contain an SSA format + * line. The second must contain an SSA header that will be assumed common to all samples. + */ + public SsaDecoder(List initializationData) { + super("SsaDecoder"); + if (initializationData != null) { + haveInitializationData = true; + String formatLine = new String(initializationData.get(0)); + Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); + parseFormatLine(formatLine); + parseHeader(new ParsableByteArray(initializationData.get(1))); + } else { + haveInitializationData = false; + } + } + + @Override + protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) { + ArrayList cues = new ArrayList<>(); + LongArray cueTimesUs = new LongArray(); + + ParsableByteArray data = new ParsableByteArray(bytes, length); + if (!haveInitializationData) { + parseHeader(data); + } + parseEventBody(data, cues, cueTimesUs); + + Cue[] cuesArray = new Cue[cues.size()]; + cues.toArray(cuesArray); + long[] cueTimesUsArray = cueTimesUs.toArray(); + return new SsaSubtitle(cuesArray, cueTimesUsArray); + } + + /** + * Parses the header of the subtitle. + * + * @param data A {@link ParsableByteArray} from which the header should be read. + */ + private void parseHeader(ParsableByteArray data) { + String currentLine; + while ((currentLine = data.readLine()) != null) { + // TODO: Parse useful data from the header. + if (currentLine.startsWith("[Events]")) { + // We've reached the event body. + return; + } + } + } + + /** + * Parses the event body of the subtitle. + * + * @param data A {@link ParsableByteArray} from which the body should be read. + * @param cues A list to which parsed cues will be added. + * @param cueTimesUs An array to which parsed cue timestamps will be added. + */ + private void parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) { + String currentLine; + while ((currentLine = data.readLine()) != null) { + if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) { + parseFormatLine(currentLine); + } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { + parseDialogueLine(currentLine, cues, cueTimesUs); + } + } + } + + /** + * Parses a format line. + * + * @param formatLine The line to parse. + */ + private void parseFormatLine(String formatLine) { + String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); + formatKeyCount = values.length; + formatStartIndex = C.INDEX_UNSET; + formatEndIndex = C.INDEX_UNSET; + formatTextIndex = C.INDEX_UNSET; + for (int i = 0; i < formatKeyCount; i++) { + String key = Util.toLowerInvariant(values[i].trim()); + switch (key) { + case "start": + formatStartIndex = i; + break; + case "end": + formatEndIndex = i; + break; + case "text": + formatTextIndex = i; + break; + default: + // Do nothing. + break; + } + } + } + + /** + * Parses a dialogue line. + * + * @param dialogueLine The line to parse. + * @param cues A list to which parsed cues will be added. + * @param cueTimesUs An array to which parsed cue timestamps will be added. + */ + private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { + if (formatKeyCount == 0) { + Log.w(TAG, "Skipping dialogue line before format: " + dialogueLine); + return; + } + + String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) + .split(",", formatKeyCount); + long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]); + if (startTimeUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping invalid timing: " + dialogueLine); + return; + } + + long endTimeUs = C.TIME_UNSET; + String endTimeString = lineValues[formatEndIndex]; + if (!endTimeString.trim().isEmpty()) { + endTimeUs = SsaDecoder.parseTimecodeUs(endTimeString); + if (endTimeUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping invalid timing: " + dialogueLine); + return; + } + } + + String text = lineValues[formatTextIndex] + .replaceAll("\\{.*?\\}", "") + .replaceAll("\\\\N", "\n") + .replaceAll("\\\\n", "\n"); + cues.add(new Cue(text)); + cueTimesUs.add(startTimeUs); + if (endTimeUs != C.TIME_UNSET) { + cues.add(null); + cueTimesUs.add(endTimeUs); + } + } + + /** + * Parses an SSA timecode string. + * + * @param timeString The string to parse. + * @return The parsed timestamp in microseconds. + */ + public static long parseTimecodeUs(String timeString) { + Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString); + if (!matcher.matches()) { + return C.TIME_UNSET; + } + long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second. + return timestampUs; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java new file mode 100644 index 0000000000..339119ed6b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -0,0 +1,72 @@ +/* + * 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.text.ssa; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.Collections; +import java.util.List; + +/** + * A representation of an SSA/ASS subtitle. + */ +/* package */ final class SsaSubtitle implements Subtitle { + + private final Cue[] cues; + private final long[] cueTimesUs; + + /** + * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cueTimesUs The cue times, in microseconds. + */ + public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { + this.cues = cues; + this.cueTimesUs = cueTimesUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); + return index < cueTimesUs.length ? index : C.INDEX_UNSET; + } + + @Override + public int getEventTimeCount() { + return cueTimesUs.length; + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < cueTimesUs.length); + return cueTimesUs[index]; + } + + @Override + public List getCues(long timeUs) { + int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); + if (index == -1 || cues[index] == null) { + // timeUs is earlier than the start of the first cue, or we have an empty cue. + return Collections.emptyList(); + } else { + return Collections.singletonList(cues[index]); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index e438aa1837..a215bf3cc9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -290,7 +290,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_DISPLAY_ALIGN); if (displayAlign != null) { - switch (displayAlign.toLowerCase()) { + switch (Util.toLowerInvariant(displayAlign)) { case "center": lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; line += height / 2; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 2a426c9c52..fe2b920933 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -33,37 +33,115 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** - * A {@link MappingTrackSelector} that allows configuration of common parameters. It is safe to call - * the methods of this class from the application thread. See {@link Parameters#Parameters()} for - * default selection parameters. + * A default {@link TrackSelector} suitable for most use cases. + * + *

    Constraint based track selection

    + * Whilst this selector supports setting specific track overrides, the recommended way of + * changing which tracks are selected is by setting {@link Parameters} that constrain the track + * selection process. For example an instance can specify a preferred language for + * the audio track, and impose constraints on the maximum video resolution that should be selected + * for adaptive playbacks. Modifying the parameters is simple: + *
    + * {@code
    + * Parameters currentParameters = trackSelector.getParameters();
    + * // Generate new parameters to prefer German audio and impose a maximum video size constraint.
    + * Parameters newParameters = currentParameters
    + *     .withPreferredAudioLanguage("de")
    + *     .withMaxVideoSize(1024, 768);
    + * // Set the new parameters on the selector.
    + * trackSelector.setParameters(newParameters);}
    + * 
    + * There are several benefits to using constraint based track selection instead of specific track + * overrides: + *
      + *
    • You can specify constraints before knowing what tracks the media provides. This can + * simplify track selection code (e.g. you don't have to listen for changes in the available + * tracks before configuring the selector).
    • + *
    • Constraints can be applied consistently across all periods in a complex piece of media, + * even if those periods contain different tracks. In contrast, a specific track override is only + * applied to periods whose tracks match those for which the override was set.
    • + *
    + * + *

    Track overrides, disabling renderers and tunneling

    + * This selector extends {@link MappingTrackSelector}, and so inherits its support for setting + * specific track overrides, disabling renderers and configuring tunneled media playback. See + * {@link MappingTrackSelector} for details. + * + *

    Extending this class

    + * This class is designed to be extensible by developers who wish to customize its behavior but do + * not wish to implement their own {@link MappingTrackSelector} or {@link TrackSelector} from + * scratch. */ public class DefaultTrackSelector extends MappingTrackSelector { /** - * Holder for available configurations for the {@link DefaultTrackSelector}. + * Constraint parameters for {@link DefaultTrackSelector}. */ public static final class Parameters { - // Audio. + // Audio + /** + * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646. + * {@code null} selects the default track, or the first track if there's no default. + */ public final String preferredAudioLanguage; - // Text. + // Text + /** + * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the + * default track if there is one, or no track otherwise. + */ public final String preferredTextLanguage; - // Video. - public final boolean allowMixedMimeAdaptiveness; - public final boolean allowNonSeamlessAdaptiveness; + // Video + /** + * Maximum allowed video width. + */ public final int maxVideoWidth; + /** + * Maximum allowed video height. + */ public final int maxVideoHeight; + /** + * Maximum video bitrate. + */ public final int maxVideoBitrate; + /** + * Whether to exceed video constraints when no selection can be made otherwise. + */ public final boolean exceedVideoConstraintsIfNecessary; - public final boolean exceedRendererCapabilitiesIfNecessary; + /** + * Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. + */ public final int viewportWidth; + /** + * Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. + */ public final int viewportHeight; - public final boolean orientationMayChange; + /** + * Whether the viewport orientation may change during playback. Constrains video tracks + * selections for adaptive playbacks so that only tracks suitable for the viewport are selected. + */ + public final boolean viewportOrientationMayChange; + + // General + /** + * Whether to allow adaptive selections containing mixed mime types. + */ + public final boolean allowMixedMimeAdaptiveness; + /** + * Whether to allow adaptive selections where adaptation may not be completely seamless. + */ + public final boolean allowNonSeamlessAdaptiveness; + /** + * Whether to exceed renderer capabilities when no selection can be made otherwise. + */ + public final boolean exceedRendererCapabilitiesIfNecessary; /** - * Constructor with default selection parameters: + * Default parameters. The default values are: *
      *
    • No preferred audio language is set.
    • *
    • No preferred text language is set.
    • @@ -73,7 +151,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
    • No max video bitrate.
    • *
    • Video constraints are exceeded if no supported selection can be made otherwise.
    • *
    • Renderer capabilities are exceeded if no supported selection can be made.
    • - *
    • No viewport width/height constraints are set.
    • + *
    • No viewport constraints are set.
    • *
    */ public Parameters() { @@ -82,29 +160,24 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * @param preferredAudioLanguage The preferred language for audio, as well as for forced text - * tracks as defined by RFC 5646. {@code null} to select the default track, or first track - * if there's no default. - * @param preferredTextLanguage The preferred language for text tracks as defined by RFC 5646. - * {@code null} to select the default track, or first track if there's no default. - * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types. - * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. - * @param maxVideoWidth Maximum allowed video width. - * @param maxVideoHeight Maximum allowed video height. - * @param maxVideoBitrate Maximum allowed video bitrate. - * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no - * selection can be made otherwise. - * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no - * selection can be made otherwise. - * @param viewportWidth Viewport width in pixels. - * @param viewportHeight Viewport height in pixels. - * @param orientationMayChange Whether orientation may change during playback. + * @param preferredAudioLanguage See {@link #preferredAudioLanguage} + * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} + * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} + * @param maxVideoWidth See {@link #maxVideoWidth} + * @param maxVideoHeight See {@link #maxVideoHeight} + * @param maxVideoBitrate See {@link #maxVideoBitrate} + * @param exceedVideoConstraintsIfNecessary See {@link #exceedVideoConstraintsIfNecessary} + * @param exceedRendererCapabilitiesIfNecessary See {@link #preferredTextLanguage} + * @param viewportWidth See {@link #viewportWidth} + * @param viewportHeight See {@link #viewportHeight} + * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, - int viewportWidth, int viewportHeight, boolean orientationMayChange) { + int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; @@ -116,17 +189,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; - this.orientationMayChange = orientationMayChange; + this.viewportOrientationMayChange = viewportOrientationMayChange; } /** - * Returns a {@link Parameters} instance with the provided preferred language for audio and - * forced text tracks. + * Returns an instance with the provided preferred language for audio and forced text tracks. * * @param preferredAudioLanguage The preferred language as defined by RFC 5646. {@code null} to * select the default track, or first track if there's no default. - * @return A {@link Parameters} instance with the provided preferred language for audio and - * forced text tracks. + * @return An instance with the provided preferred language for audio and forced text tracks. */ public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); @@ -136,15 +207,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided preferred language for text tracks. + * Returns an instance with the provided preferred language for text tracks. * * @param preferredTextLanguage The preferred language as defined by RFC 5646. {@code null} to * select the default track, or no track if there's no default. - * @return A {@link Parameters} instance with the provided preferred language for text tracks. + * @return An instance with the provided preferred language for text tracks. */ public Parameters withPreferredTextLanguage(String preferredTextLanguage) { preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); @@ -154,14 +225,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided mixed mime adaptiveness allowance. + * Returns an instance with the provided mixed mime adaptiveness allowance. * * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types. - * @return A {@link Parameters} instance with the provided mixed mime adaptiveness allowance. + * @return An instance with the provided mixed mime adaptiveness allowance. */ public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { @@ -170,14 +241,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided seamless adaptiveness allowance. + * Returns an instance with the provided seamless adaptiveness allowance. * * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. - * @return A {@link Parameters} instance with the provided seamless adaptiveness allowance. + * @return An instance with the provided seamless adaptiveness allowance. */ public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { @@ -186,15 +257,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided max video size. + * Returns an instance with the provided max video size. * * @param maxVideoWidth The max video width. * @param maxVideoHeight The max video width. - * @return A {@link Parameters} instance with the provided max video size. + * @return An instance with the provided max video size. */ public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { @@ -203,14 +274,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided max video bitrate. + * Returns an instance with the provided max video bitrate. * * @param maxVideoBitrate The max video bitrate. - * @return A {@link Parameters} instance with the provided max video bitrate. + * @return An instance with the provided max video bitrate. */ public Parameters withMaxVideoBitrate(int maxVideoBitrate) { if (maxVideoBitrate == this.maxVideoBitrate) { @@ -219,13 +290,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** * Equivalent to {@code withMaxVideoSize(1279, 719)}. * - * @return A {@link Parameters} instance with maximum standard definition as maximum video size. + * @return An instance with maximum standard definition as maximum video size. */ public Parameters withMaxVideoSizeSd() { return withMaxVideoSize(1279, 719); @@ -234,20 +305,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. * - * @return A {@link Parameters} instance without video size constraints. + * @return An instance without video size constraints. */ public Parameters withoutVideoSizeConstraints() { return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); } /** - * Returns a {@link Parameters} instance with the provided - * {@code exceedVideoConstraintsIfNecessary} value. + * Returns an instance with the provided {@code exceedVideoConstraintsIfNecessary} value. * * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no * selection can be made otherwise. - * @return A {@link Parameters} instance with the provided - * {@code exceedVideoConstraintsIfNecessary} value. + * @return An instance with the provided {@code exceedVideoConstraintsIfNecessary} value. */ public Parameters withExceedVideoConstraintsIfNecessary( boolean exceedVideoConstraintsIfNecessary) { @@ -257,17 +326,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided - * {@code exceedRendererCapabilitiesIfNecessary} value. + * Returns an instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value. * * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no * selection can be made otherwise. - * @return A {@link Parameters} instance with the provided - * {@code exceedRendererCapabilitiesIfNecessary} value. + * @return An instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value. */ public Parameters withExceedRendererCapabilitiesIfNecessary( boolean exceedRendererCapabilitiesIfNecessary) { @@ -277,48 +344,47 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided viewport size. + * Returns an instance with the provided viewport size. * * @param viewportWidth Viewport width in pixels. * @param viewportHeight Viewport height in pixels. - * @param orientationMayChange Whether orientation may change during playback. - * @return A {@link Parameters} instance with the provided viewport size. + * @param viewportOrientationMayChange Whether orientation may change during playback. + * @return An instance with the provided viewport size. */ public Parameters withViewportSize(int viewportWidth, int viewportHeight, - boolean orientationMayChange) { + boolean viewportOrientationMayChange) { if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight - && orientationMayChange == this.orientationMayChange) { + && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance where the viewport size is obtained from the provided - * {@link Context}. + * Returns an instance where the viewport size is obtained from the provided {@link Context}. * * @param context The context to obtain the viewport size from. - * @param orientationMayChange Whether orientation may change during playback. - * @return A {@link Parameters} instance where the viewport size is obtained from the provided - * {@link Context}. + * @param viewportOrientationMayChange Whether orientation may change during playback. + * @return An instance where the viewport size is obtained from the provided {@link Context}. */ - public Parameters withViewportSizeFromContext(Context context, boolean orientationMayChange) { + public Parameters withViewportSizeFromContext(Context context, + boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. Point viewportSize = Util.getPhysicalDisplaySize(context); - return withViewportSize(viewportSize.x, viewportSize.y, orientationMayChange); + return withViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); } /** * Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}. * - * @return A {@link Parameters} instance without viewport size constraints. + * @return An instance without viewport size constraints. */ public Parameters withoutViewportSizeConstraints() { return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); @@ -338,7 +404,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary - && orientationMayChange == other.orientationMayChange + && viewportOrientationMayChange == other.viewportOrientationMayChange && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight && maxVideoBitrate == other.maxVideoBitrate && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) @@ -356,7 +422,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); - result = 31 * result + (orientationMayChange ? 1 : 0); + result = 31 * result + (viewportOrientationMayChange ? 1 : 0); result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; return result; @@ -443,12 +509,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { if (!selectedVideoTracks) { rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, - params.exceedRendererCapabilitiesIfNecessary); + rendererTrackGroupArrays[i], rendererFormatSupports[i], params, + adaptiveTrackSelectionFactory); selectedVideoTracks = rendererTrackSelections[i] != null; } seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0; @@ -465,8 +527,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { case C.TRACK_TYPE_AUDIO: if (!selectedAudioTracks) { rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredAudioLanguage, - params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, + rendererFormatSupports[i], params, seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); selectedAudioTracks = rendererTrackSelections[i] != null; } @@ -474,15 +535,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { case C.TRACK_TYPE_TEXT: if (!selectedTextTracks) { rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredTextLanguage, - params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); + rendererFormatSupports[i], params); selectedTextTracks = rendererTrackSelections[i] != null; } break; default: rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), - rendererTrackGroupArrays[i], rendererFormatSupports[i], - params.exceedRendererCapabilitiesIfNecessary); + rendererTrackGroupArrays[i], rendererFormatSupports[i], params); break; } } @@ -491,42 +550,48 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Video track selection implementation. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for a video renderer. + * + * @param rendererCapabilities The {@link RendererCapabilities} for the renderer. + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or + * null if a fixed track selection is required. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, - int viewportWidth, int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveTrackSelectionFactory, boolean exceedConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException { + TrackGroupArray groups, int[][] formatSupport, Parameters params, + TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { TrackSelection selection = null; if (adaptiveTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, - maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness, - allowMixedMimeAdaptiveness, viewportWidth, viewportHeight, - orientationMayChange, adaptiveTrackSelectionFactory); + params, adaptiveTrackSelectionFactory); } if (selection == null) { - selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange, - exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary); + selection = selectFixedVideoTrack(groups, formatSupport, params); } return selection; } private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, - int viewportWidth, int viewportHeight, boolean orientationMayChange, + TrackGroupArray groups, int[][] formatSupport, Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { - int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness + int requiredAdaptiveSupport = params.allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) : RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness + boolean allowMixedMimeTypes = params.allowMixedMimeAdaptiveness && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0; for (int i = 0; i < groups.length; i++) { TrackGroup group = groups.get(i); int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i], - allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange); + allowMixedMimeTypes, requiredAdaptiveSupport, params.maxVideoWidth, params.maxVideoHeight, + params.maxVideoBitrate, params.viewportWidth, params.viewportHeight, + params.viewportOrientationMayChange); if (adaptiveTracks.length > 0) { return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); } @@ -537,13 +602,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveVideoTracksForGroup(TrackGroup group, int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight, - boolean orientationMayChange) { + boolean viewportOrientationMayChange) { if (group.length < 2) { return NO_TRACKS; } List selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth, - viewportHeight, orientationMayChange); + viewportHeight, viewportOrientationMayChange); if (selectedTrackIndices.size() < 2) { return NO_TRACKS; } @@ -614,9 +679,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, - int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, - int viewportWidth, int viewportHeight, boolean orientationMayChange, - boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) { + int[][] formatSupport, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -625,16 +688,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, - viewportWidth, viewportHeight, orientationMayChange); + params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) - && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) - && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); - if (!isWithinConstraints && !exceedConstraintsIfNecessary) { + && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth) + && (format.height == Format.NO_VALUE || format.height <= params.maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= params.maxVideoBitrate); + if (!isWithinConstraints && !params.exceedVideoConstraintsIfNecessary) { // Track should not be selected. continue; } @@ -689,9 +753,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio track selection implementation. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for an audio renderer. + * + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or + * null if a fixed track selection is required. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary, - boolean allowMixedMimeAdaptiveness, TrackSelection.Factory adaptiveTrackSelectionFactory) { + Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) { int selectedGroupIndex = C.INDEX_UNSET; int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; @@ -699,10 +775,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex], - preferredAudioLanguage, format); + params.preferredAudioLanguage, format); if (trackScore > selectedTrackScore) { selectedGroupIndex = groupIndex; selectedTrackIndex = trackIndex; @@ -720,7 +797,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (adaptiveTrackSelectionFactory != null) { // If the group of the track with the highest score allows it, try to enable adaptation. int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup, - formatSupport[selectedGroupIndex], allowMixedMimeAdaptiveness); + formatSupport[selectedGroupIndex], params.allowMixedMimeAdaptiveness); if (adaptiveTracks.length > 0) { return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup, adaptiveTracks); @@ -804,9 +881,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text track selection implementation. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for a text renderer. + * + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredTextLanguage, String preferredAudioLanguage, - boolean exceedRendererCapabilitiesIfNecessary) { + Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -814,12 +901,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; - if (formatHasLanguage(format, preferredTextLanguage)) { + if (formatHasLanguage(format, params.preferredTextLanguage)) { if (isDefault) { trackScore = 6; } else if (!isForced) { @@ -833,7 +921,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } else if (isDefault) { trackScore = 3; } else if (isForced) { - if (formatHasLanguage(format, preferredAudioLanguage)) { + if (formatHasLanguage(format, params.preferredAudioLanguage)) { trackScore = 2; } else { trackScore = 1; @@ -859,8 +947,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General track selection methods. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for a renderer whose type is neither video, audio or text. + * + * @param trackType The type of the renderer. + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) { + int[][] formatSupport, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -868,7 +968,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; int trackScore = isDefault ? 2 : 1; @@ -887,12 +988,34 @@ public class DefaultTrackSelector extends MappingTrackSelector { : new FixedTrackSelection(selectedGroup, selectedTrackIndex); } + /** + * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from + * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is + * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set + * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * + * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}. + * @param allowExceedsCapabilities Whether to return true if the format support component of the + * value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or + * if {@code allowExceedsCapabilities} is set and the format support component is + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + */ protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** + * Returns whether a {@link Format} specifies a particular language, or {@code false} if + * {@code language} is null. + * + * @param format The {@link Format}. + * @param language The language. + * @return Whether the format specifies the language, or {@code false} if {@code language} is + * null. + */ protected static boolean formatHasLanguage(Format format, String language) { return language != null && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 3499efdb16..45ac9eab6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -20,6 +20,8 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; @@ -31,10 +33,260 @@ import java.util.Map; /** * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s - * and renderers, and then from that mapping create a {@link TrackSelection} for each renderer. + * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each + * renderer. + * + *

    Track overrides

    + * Mapping track selectors support overriding of track selections for each renderer. To specify an + * override for a renderer it's first necessary to obtain the tracks that have been mapped to it: + *
    + * {@code
    + * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
    + * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
    + *     : mappedTrackInfo.getTrackGroups(rendererIndex);}
    + * 
    + * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so + * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the + * player can be used to determine when the current tracks (and therefore the mapping) changes. If + * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query + * the properties of the available tracks to determine the {@code groupIndex} of the track group you + * want to select and the {@code trackIndices} within it. You can then create and set the override: + *
    + * {@code
    + * trackSelector.setSelectionOverride(rendererIndex, rendererTrackGroups,
    + *     new SelectionOverride(trackSelectionFactory, groupIndex, trackIndices));}
    + * 
    + * where {@code trackSelectionFactory} is a {@link TrackSelection.Factory} for generating concrete + * {@link TrackSelection} instances for the override. It's also possible to pass {@code null} as the + * selection override if you don't want any tracks to be selected. + *

    + * Note that an override applies only when the track groups available to the renderer match the + * {@link TrackGroupArray} for which the override was specified. Overrides can be cleared using + * the {@code clearSelectionOverride} methods. + * + *

    Disabling renderers

    + * Renderers can be disabled using {@link #setRendererDisabled(int, boolean)}. Disabling a renderer + * differs from setting a {@code null} override because the renderer is disabled unconditionally, + * whereas a {@code null} override is applied only when the track groups available to the renderer + * match the {@link TrackGroupArray} for which it was specified. + * + *

    Tunneling

    + * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks + * support it. See {@link #setTunnelingAudioSessionId(int)} for more details. */ public abstract class MappingTrackSelector extends TrackSelector { + /** + * Provides mapped track information for each renderer. + */ + public static final class MappedTrackInfo { + + /** + * The renderer does not have any associated tracks. + */ + public static final int RENDERER_SUPPORT_NO_TRACKS = 0; + /** + * The renderer has associated tracks, but all are of unsupported types. + */ + public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; + /** + * The renderer has associated tracks and at least one is of a supported type, but all of the + * tracks whose types are supported exceed the renderer's capabilities. + */ + public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; + /** + * The renderer has associated tracks and can play at least one of them. + */ + public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; + + /** + * The number of renderers to which tracks are mapped. + */ + public final int length; + + private final int[] rendererTrackTypes; + private final TrackGroupArray[] trackGroups; + private final int[] mixedMimeTypeAdaptiveSupport; + private final int[][][] formatSupport; + private final TrackGroupArray unassociatedTrackGroups; + + /** + * @param rendererTrackTypes The track type supported by each renderer. + * @param trackGroups The {@link TrackGroup}s mapped to each renderer. + * @param mixedMimeTypeAdaptiveSupport The result of + * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each + * mapped track, indexed by renderer index, track group index and track index (in that + * order). + * @param unassociatedTrackGroups Any {@link TrackGroup}s not mapped to any renderer. + */ + /* package */ MappedTrackInfo(int[] rendererTrackTypes, + TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport, + int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) { + this.rendererTrackTypes = rendererTrackTypes; + this.trackGroups = trackGroups; + this.formatSupport = formatSupport; + this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport; + this.unassociatedTrackGroups = unassociatedTrackGroups; + this.length = trackGroups.length; + } + + /** + * Returns the {@link TrackGroup}s mapped to the renderer at the specified index. + * + * @param rendererIndex The renderer index. + * @return The corresponding {@link TrackGroup}s. + */ + public TrackGroupArray getTrackGroups(int rendererIndex) { + return trackGroups[rendererIndex]; + } + + /** + * Returns the extent to which a renderer can play the tracks in the track groups mapped to it. + * + * @param rendererIndex The renderer index. + * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, + * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, + * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + */ + public int getRendererSupport(int rendererIndex) { + int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + int[][] rendererFormatSupport = formatSupport[rendererIndex]; + for (int i = 0; i < rendererFormatSupport.length; i++) { + for (int j = 0; j < rendererFormatSupport[i].length; j++) { + int trackRendererSupport; + switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { + case RendererCapabilities.FORMAT_HANDLED: + return RENDERER_SUPPORT_PLAYABLE_TRACKS; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; + break; + default: + trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; + break; + } + bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); + } + } + return bestRendererSupport; + } + + /** + * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all + * renderers of the specified track type. If no renderers exist for the specified type then + * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned. + * + * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, + * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, + * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + */ + public int getTrackTypeRendererSupport(int trackType) { + int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + for (int i = 0; i < length; i++) { + if (rendererTrackTypes[i] == trackType) { + bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); + } + } + return bestRendererSupport; + } + + /** + * Returns the extent to which an individual track is supported by the renderer. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group to which the track belongs. + * @param trackIndex The index of the track within the track group. + * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. + */ + public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { + return formatSupport[rendererIndex][groupIndex][trackIndex] + & RendererCapabilities.FORMAT_SUPPORT_MASK; + } + + /** + * Returns the extent to which a renderer supports adaptation between supported tracks in a + * specified {@link TrackGroup}. + *

    + * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. + * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. + * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if + * {@code includeCapabilitiesExceededTracks} is set to {@code true}. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group. + * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the + * renderer should be included when determining support. False otherwise. + * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, + * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and + * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + */ + public int getAdaptiveSupport(int rendererIndex, int groupIndex, + boolean includeCapabilitiesExceededTracks) { + int trackCount = trackGroups[rendererIndex].get(groupIndex).length; + // Iterate over the tracks in the group, recording the indices of those to consider. + int[] trackIndices = new int[trackCount]; + int trackIndexCount = 0; + for (int i = 0; i < trackCount; i++) { + int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i); + if (fixedSupport == RendererCapabilities.FORMAT_HANDLED + || (includeCapabilitiesExceededTracks + && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { + trackIndices[trackIndexCount++] = i; + } + } + trackIndices = Arrays.copyOf(trackIndices, trackIndexCount); + return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices); + } + + /** + * Returns the extent to which a renderer supports adaptation between specified tracks within + * a {@link TrackGroup}. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group. + * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, + * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and + * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + */ + public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { + int handledTrackCount = 0; + int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; + boolean multipleMimeTypes = false; + String firstSampleMimeType = null; + for (int i = 0; i < trackIndices.length; i++) { + int trackIndex = trackIndices[i]; + String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex) + .sampleMimeType; + if (handledTrackCount++ == 0) { + firstSampleMimeType = sampleMimeType; + } else { + multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType); + } + adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i] + & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); + } + return multipleMimeTypes + ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex]) + : adaptiveSupport; + } + + /** + * Returns {@link TrackGroup}s not mapped to any renderer. + */ + public TrackGroupArray getUnassociatedTrackGroups() { + return unassociatedTrackGroups; + } + + } + /** * A track selection override. */ @@ -47,8 +299,8 @@ public abstract class MappingTrackSelector extends TrackSelector { /** * @param factory A factory for creating selections from this override. - * @param groupIndex The overriding group index. - * @param tracks The overriding track indices within the group. + * @param groupIndex The overriding track group index. + * @param tracks The overriding track indices within the track group. */ public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) { this.factory = factory; @@ -60,7 +312,7 @@ public abstract class MappingTrackSelector extends TrackSelector { /** * Creates an selection from this override. * - * @param groups The groups whose selection is being overridden. + * @param groups The track groups whose selection is being overridden. * @return The selection. */ public TrackSelection createTrackSelection(TrackGroupArray groups) { @@ -94,7 +346,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Returns the mapping information associated with the current track selections, or null if no + * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. */ public final MappedTrackInfo getCurrentMappedTrackInfo() { @@ -102,7 +354,8 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Sets whether the renderer at the specified index is disabled. + * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents the + * selector from selecting any tracks for it. * * @param rendererIndex The renderer index. * @param disabled Whether the renderer is disabled. @@ -127,16 +380,22 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Overrides the track selection for the renderer at a specified index. + * Overrides the track selection for the renderer at the specified index. *

    - * When the {@link TrackGroupArray} available to the renderer at the specified index matches the - * one provided, the override is applied. When the {@link TrackGroupArray} does not match, the - * override has no effect. The override replaces any previous override for the renderer and the - * provided {@link TrackGroupArray}. + * When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the override + * is applied. When the {@link TrackGroupArray} does not match, the override has no effect. The + * override replaces any previous override for the specified {@link TrackGroupArray} for the + * specified {@link Renderer}. *

    - * Passing a {@code null} override will explicitly disable the renderer. To remove overrides use - * {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link #clearSelectionOverrides(int)} - * or {@link #clearSelectionOverrides()}. + * Passing a {@code null} override will cause the renderer to be disabled when the + * {@link TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} + * does not match a {@code null} override has no effect. Hence a {@code null} override differs + * from disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the + * renderer is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as + * {@link #setRendererDisabled(int, boolean)} disables the renderer unconditionally. + *

    + * To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, + * {@link #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}. * * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be applied. @@ -201,7 +460,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Clears all track selection override for the specified renderer. + * Clears all track selection overrides for the specified renderer. * * @param rendererIndex The renderer index. */ @@ -216,7 +475,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Clears all track selection overrides. + * Clears all track selection overrides for all renderers. */ public final void clearSelectionOverrides() { if (selectionOverrides.size() == 0) { @@ -338,15 +597,15 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Given an array of renderers and a set of {@link TrackGroup}s mapped to each of them, provides a - * {@link TrackSelection} per renderer. + * Given an array of renderer capabilities and the {@link TrackGroupArray}s mapped to each of + * them, provides a {@link TrackSelection} per renderer. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which * {@link TrackSelection}s are to be generated. - * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry - * corresponds to the renderer of equal index in {@code renderers}. - * @param rendererFormatSupports Maps every available track to a specific level of support as - * defined by the renderer {@code FORMAT_*} constants. + * @param rendererTrackGroupArrays The {@link TrackGroupArray}s mapped to each of the renderers. + * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for + * each mapped track, indexed by renderer index, track group index and track index (in that + * order). * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected abstract TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, @@ -354,9 +613,9 @@ public abstract class MappingTrackSelector extends TrackSelector { throws ExoPlaybackException; /** - * Finds the renderer to which the provided {@link TrackGroup} should be associated. + * Finds the renderer to which the provided {@link TrackGroup} should be mapped. *

    - * A {@link TrackGroup} is associated to a renderer that reports + * A {@link TrackGroup} is mapped to the renderer that reports * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group, * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In @@ -364,13 +623,13 @@ public abstract class MappingTrackSelector extends TrackSelector { * lowest index is associated. *

    * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the - * tracks in the group, then {@code renderers.length} is returned to indicate that no association - * was made. + * tracks in the group, then {@code renderers.length} is returned to indicate that the group was + * not mapped to any renderer. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @param group The {@link TrackGroup} whose associated renderer is to be found. - * @return The index of the associated renderer, or {@code renderers.length} if no - * association was made. + * @param group The track group to map to a renderer. + * @return The index of the renderer to which the track group was mapped, or + * {@code renderers.length} if it was not mapped to any renderer. * @throws ExoPlaybackException If an error occurs finding a renderer. */ private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) @@ -400,7 +659,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * {@link TrackGroup}, returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderer. - * @param group The {@link TrackGroup} to evaluate. + * @param group The track group to evaluate. * @return An array containing the result of calling * {@link RendererCapabilities#supportsFormat} for each track in the group. * @throws ExoPlaybackException If an error occurs determining the format support. @@ -520,214 +779,4 @@ public abstract class MappingTrackSelector extends TrackSelector { return true; } - /** - * Provides track information for each renderer. - */ - public static final class MappedTrackInfo { - - /** - * The renderer does not have any associated tracks. - */ - public static final int RENDERER_SUPPORT_NO_TRACKS = 0; - /** - * The renderer has associated tracks, but all are of unsupported types. - */ - public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; - /** - * The renderer has associated tracks and at least one is of a supported type, but all of the - * tracks whose types are supported exceed the renderer's capabilities. - */ - public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; - /** - * The renderer has associated tracks and can play at least one of them. - */ - public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; - - /** - * The number of renderers to which tracks are mapped. - */ - public final int length; - - private final int[] rendererTrackTypes; - private final TrackGroupArray[] trackGroups; - private final int[] mixedMimeTypeAdaptiveSupport; - private final int[][][] formatSupport; - private final TrackGroupArray unassociatedTrackGroups; - - /** - * @param rendererTrackTypes The track type supported by each renderer. - * @param trackGroups The {@link TrackGroupArray}s for each renderer. - * @param mixedMimeTypeAdaptiveSupport The result of - * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each - * track, indexed by renderer index, group index and track index (in that order). - * @param unassociatedTrackGroups Contains {@link TrackGroup}s not associated with any renderer. - */ - /* package */ MappedTrackInfo(int[] rendererTrackTypes, - TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport, - int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) { - this.rendererTrackTypes = rendererTrackTypes; - this.trackGroups = trackGroups; - this.formatSupport = formatSupport; - this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport; - this.unassociatedTrackGroups = unassociatedTrackGroups; - this.length = trackGroups.length; - } - - /** - * Returns the array of {@link TrackGroup}s associated to the renderer at a specified index. - * - * @param rendererIndex The renderer index. - * @return The corresponding {@link TrackGroup}s. - */ - public TrackGroupArray getTrackGroups(int rendererIndex) { - return trackGroups[rendererIndex]; - } - - /** - * Returns the extent to which a renderer can support playback of the tracks associated to it. - * - * @param rendererIndex The renderer index. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, - * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. - */ - public int getRendererSupport(int rendererIndex) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = formatSupport[rendererIndex]; - for (int i = 0; i < rendererFormatSupport.length; i++) { - for (int j = 0; j < rendererFormatSupport[i].length; j++) { - int trackRendererSupport; - switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { - case RendererCapabilities.FORMAT_HANDLED: - return RENDERER_SUPPORT_PLAYABLE_TRACKS; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; - break; - default: - trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; - break; - } - bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); - } - } - return bestRendererSupport; - } - - /** - * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all - * renderers of the specified track type. If no renderers exist for the specified type then - * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned. - * - * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, - * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. - */ - public int getTrackTypeRendererSupport(int trackType) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - for (int i = 0; i < length; i++) { - if (rendererTrackTypes[i] == trackType) { - bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); - } - } - return bestRendererSupport; - } - - /** - * Returns the extent to which the format of an individual track is supported by the renderer. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group to which the track belongs. - * @param trackIndex The index of the track within the group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. - */ - public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { - return formatSupport[rendererIndex][groupIndex][trackIndex] - & RendererCapabilities.FORMAT_SUPPORT_MASK; - } - - /** - * Returns the extent to which the renderer supports adaptation between supported tracks in a - * specified {@link TrackGroup}. - *

    - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if - * {@code includeCapabilitiesExceededTracks} is set to {@code true}. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group. - * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the - * renderer should be included when determining support. False otherwise. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. - */ - public int getAdaptiveSupport(int rendererIndex, int groupIndex, - boolean includeCapabilitiesExceededTracks) { - int trackCount = trackGroups[rendererIndex].get(groupIndex).length; - // Iterate over the tracks in the group, recording the indices of those to consider. - int[] trackIndices = new int[trackCount]; - int trackIndexCount = 0; - for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i); - if (fixedSupport == RendererCapabilities.FORMAT_HANDLED - || (includeCapabilitiesExceededTracks - && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { - trackIndices[trackIndexCount++] = i; - } - } - trackIndices = Arrays.copyOf(trackIndices, trackIndexCount); - return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices); - } - - /** - * Returns the extent to which the renderer supports adaptation between specified tracks within - * a {@link TrackGroup}. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. - */ - public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { - int handledTrackCount = 0; - int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean multipleMimeTypes = false; - String firstSampleMimeType = null; - for (int i = 0; i < trackIndices.length; i++) { - int trackIndex = trackIndices[i]; - String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex) - .sampleMimeType; - if (handledTrackCount++ == 0) { - firstSampleMimeType = sampleMimeType; - } else { - multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType); - } - adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); - } - return multipleMimeTypes - ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex]) - : adaptiveSupport; - } - - /** - * Returns the {@link TrackGroup}s not associated with any renderer. - */ - public TrackGroupArray getUnassociatedTrackGroups() { - return unassociatedTrackGroups; - } - - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index 6c9fbfcb00..a26fee6f78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -16,19 +16,74 @@ package com.google.android.exoplayer2.trackselection; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroupArray; -/** Selects tracks to be consumed by available renderers. */ +/** + * The component of an {@link ExoPlayer} responsible for selecting tracks to be consumed by each of + * the player's {@link Renderer}s. The {@link DefaultTrackSelector} implementation should be + * suitable for most use cases. + * + *

    Interactions with the player

    + * The following interactions occur between the player and its track selector during playback. + *

    + *

      + *
    • When the player is created it will initialize the track selector by calling + * {@link #init(InvalidationListener)}.
    • + *
    • When the player needs to make a track selection it will call + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)}. This typically occurs at the + * start of playback, when the player starts to buffer a new period of the media being played, + * and when the track selector invalidates its previous selections.
    • + *
    • The player may perform a track selection well in advance of the selected tracks becoming + * active, where active is defined to mean that the renderers are actually consuming media + * corresponding to the selection that was made. For example when playing media containing + * multiple periods, the track selection for a period is made when the player starts to buffer + * that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the + * selection will occur approximately 30 seconds in advance of it becoming active. In fact the + * selection may never become active, for example if the user seeks to some other period of the + * media during the 30 second gap. The player indicates to the track selector when a selection + * it has previously made becomes active by calling {@link #onSelectionActivated(Object)}.
    • + *
    • If the track selector wishes to indicate to the player that selections it has previously + * made are invalid, it can do so by calling + * {@link InvalidationListener#onTrackSelectionsInvalidated()} on the + * {@link InvalidationListener} that was passed to {@link #init(InvalidationListener)}. A + * track selector may wish to do this if its configuration has changed, for example if it now + * wishes to prefer audio tracks in a particular language. This will trigger the player to make + * new track selections. Note that the player will have to re-buffer in the case that the new + * track selection for the currently playing period differs from the one that was invalidated. + *
    • + *
    + * + *

    Renderer configuration

    + * The {@link TrackSelectorResult} returned by + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} contains not only + * {@link TrackSelection}s for each renderer, but also {@link RendererConfiguration}s defining + * configuration parameters that the renderers should apply when consuming the corresponding media. + * Whilst it may seem counter-intuitive for a track selector to also specify renderer configuration + * information, in practice the two are tightly bound together. It may only be possible to play a + * certain combination tracks if the renderers are configured in a particular way. Equally, it may + * only be possible to configure renderers in a particular way if certain tracks are selected. Hence + * it makes sense to determined the track selection and corresponding renderer configurations in a + * single step. + * + *

    Threading model

    + * All calls made by the player into the track selector are on the player's internal playback + * thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()} + * from any thread. + */ public abstract class TrackSelector { /** - * Notified when previous selections by a {@link TrackSelector} are no longer valid. + * Notified when selections previously made by a {@link TrackSelector} are no longer valid. */ public interface InvalidationListener { /** - * Called by a {@link TrackSelector} when previous selections are no longer valid. + * Called by a {@link TrackSelector} to indicate that selections it has previously made are no + * longer valid. May be called from any thread. */ void onTrackSelectionsInvalidated(); @@ -37,16 +92,17 @@ public abstract class TrackSelector { private InvalidationListener listener; /** - * Initializes the selector. + * Called by the player to initialize the selector. * - * @param listener A listener for the selector. + * @param listener An invalidation listener that the selector can call to indicate that selections + * it has previously made are no longer valid. */ public final void init(InvalidationListener listener) { this.listener = listener; } /** - * Performs a track selection for renderers. + * Called by the player to perform a track selection. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks * are to be selected. @@ -58,15 +114,16 @@ public abstract class TrackSelector { TrackGroupArray trackGroups) throws ExoPlaybackException; /** - * Called when a {@link TrackSelectorResult} previously generated by + * Called by the player when a {@link TrackSelectorResult} previously generated by * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} is activated. * - * @param info The value of {@link TrackSelectorResult#info} in the activated result. + * @param info The value of {@link TrackSelectorResult#info} in the activated selection. */ public abstract void onSelectionActivated(Object info); /** - * Invalidates all previously generated track selections. + * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously + * generated track selections. */ protected final void invalidate() { if (listener != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 5cdb157570..cab9a689be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -25,11 +25,11 @@ import com.google.android.exoplayer2.util.Util; public final class TrackSelectorResult { /** - * The groups provided to the {@link TrackSelector}. + * The track groups that were provided to the {@link TrackSelector}. */ public final TrackGroupArray groups; /** - * A {@link TrackSelectionArray} containing the selection for each renderer. + * A {@link TrackSelectionArray} containing the track selection for each renderer. */ public final TrackSelectionArray selections; /** @@ -43,10 +43,10 @@ public final class TrackSelectorResult { public final RendererConfiguration[] rendererConfigurations; /** - * @param groups The groups provided to the {@link TrackSelector}. + * @param groups The track groups provided to the {@link TrackSelector}. * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. * @param info An opaque object that will be returned to - * {@link TrackSelector#onSelectionActivated(Object)} should the selections be activated. + * {@link TrackSelector#onSelectionActivated(Object)} should the selection be activated. * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used * with the selections. */ @@ -62,7 +62,7 @@ public final class TrackSelectorResult { * Returns whether this result is equivalent to {@code other} for all renderers. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} - * will be returned in all cases. + * will be returned. * @return Whether this result is equivalent to {@code other} for all renderers. */ public boolean isEquivalent(TrackSelectorResult other) { @@ -83,9 +83,10 @@ public final class TrackSelectorResult { * renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} - * will be returned in all cases. + * will be returned. * @param index The renderer index to check for equivalence. - * @return Whether this result is equivalent to {@code other} for all renderers. + * @return Whether this result is equivalent to {@code other} for the renderer at the specified + * index. */ public boolean isEquivalent(TrackSelectorResult other, int index) { if (other == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java index 08b42533cc..f5aa81f325 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Allocation.java @@ -25,29 +25,22 @@ public final class Allocation { /** * The array containing the allocated space. The allocated space might not be at the start of the - * array, and so {@link #translateOffset(int)} method must be used when indexing into it. + * array, and so {@link #offset} must be used when indexing into it. */ public final byte[] data; - private final int offset; + /** + * The offset of the allocated space in {@link #data}. + */ + public final int offset; /** * @param data The array containing the allocated space. - * @param offset The offset of the allocated space within the array. + * @param offset The offset of the allocated space in {@code data}. */ public Allocation(byte[] data, int offset) { this.data = data; this.offset = offset; } - /** - * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset. - * - * @param offset The zero-based offset to translate. - * @return The corresponding offset in {@link #data}. - */ - public int translateOffset(int offset) { - return this.offset + offset; - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index d3c63b4454..ab1542c7a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -68,7 +68,7 @@ public final class DataSpec { * The position of the data when read from {@link #uri}. *

    * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location - * of a subset of the underyling data. + * of a subset of the underlying data. */ public final long position; /** @@ -187,4 +187,31 @@ public final class DataSpec { + ", " + position + ", " + length + ", " + key + ", " + flags + "]"; } + /** + * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec. The + * subrange includes data from the offset up to the end of this DataSpec. + * + * @param offset The offset of the subrange. + * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset) { + return subrange(offset, length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length - offset); + } + + /** + * Returns a {@link DataSpec} that represents a subrange of the data defined by this DataSpec. + * + * @param offset The offset of the subrange. + * @param length The length of the subrange. + * @return A {@link DataSpec} that represents a subrange of the data defined by this DataSpec. + */ + public DataSpec subrange(long offset, long length) { + if (offset == 0 && this.length == length) { + return this; + } else { + return new DataSpec(uri, postBody, absoluteStreamPosition + offset, position + offset, length, + key, flags); + } + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 20f28e7a7d..db04b2580e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.upstream; import android.os.Handler; -import android.os.SystemClock; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.SlidingPercentile; /** @@ -37,6 +37,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private final Handler eventHandler; private final EventListener eventListener; private final SlidingPercentile slidingPercentile; + private final Clock clock; private int streamCount; private long sampleStartTimeMs; @@ -55,9 +56,15 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) { + this(eventHandler, eventListener, maxWeight, Clock.DEFAULT); + } + + public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight, + Clock clock) { this.eventHandler = eventHandler; this.eventListener = eventListener; this.slidingPercentile = new SlidingPercentile(maxWeight); + this.clock = clock; bitrateEstimate = NO_ESTIMATE; } @@ -69,7 +76,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList @Override public synchronized void onTransferStart(Object source, DataSpec dataSpec) { if (streamCount == 0) { - sampleStartTimeMs = SystemClock.elapsedRealtime(); + sampleStartTimeMs = clock.elapsedRealtime(); } streamCount++; } @@ -82,7 +89,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList @Override public synchronized void onTransferEnd(Object source) { Assertions.checkState(streamCount > 0); - long nowMs = SystemClock.elapsedRealtime(); + long nowMs = clock.elapsedRealtime(); int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs); totalElapsedTimeMs += sampleElapsedTimeMs; totalBytesTransferred += sampleBytesTransferred; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 9d13383a56..cbb8ba92a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -17,9 +17,11 @@ package com.google.android.exoplayer2.upstream; import android.content.Context; import android.net.Uri; +import android.util.Log; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; /** * A {@link DataSource} that supports multiple URI schemes. The supported schemes are: @@ -30,6 +32,8 @@ import java.io.IOException; * local file URI). *

  • asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). + *
  • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an + * explicit dependency on ExoPlayer's RTMP extension.
  • *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if * constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or * any other schemes supported by a base data source if constructed using @@ -38,13 +42,22 @@ import java.io.IOException; */ public final class DefaultDataSource implements DataSource { + private static final String TAG = "DefaultDataSource"; + private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; + private static final String SCHEME_RTMP = "rtmp"; + + private final Context context; + private final TransferListener listener; private final DataSource baseDataSource; - private final DataSource fileDataSource; - private final DataSource assetDataSource; - private final DataSource contentDataSource; + + // Lazily initialized. + private DataSource fileDataSource; + private DataSource assetDataSource; + private DataSource contentDataSource; + private DataSource rtmpDataSource; private DataSource dataSource; @@ -95,10 +108,9 @@ public final class DefaultDataSource implements DataSource { */ public DefaultDataSource(Context context, TransferListener listener, DataSource baseDataSource) { + this.context = context.getApplicationContext(); + this.listener = listener; this.baseDataSource = Assertions.checkNotNull(baseDataSource); - this.fileDataSource = new FileDataSource(listener); - this.assetDataSource = new AssetDataSource(context, listener); - this.contentDataSource = new ContentDataSource(context, listener); } @Override @@ -108,14 +120,16 @@ public final class DefaultDataSource implements DataSource { String scheme = dataSpec.uri.getScheme(); if (Util.isLocalFileUri(dataSpec.uri)) { if (dataSpec.uri.getPath().startsWith("/android_asset/")) { - dataSource = assetDataSource; + dataSource = getAssetDataSource(); } else { - dataSource = fileDataSource; + dataSource = getFileDataSource(); } } else if (SCHEME_ASSET.equals(scheme)) { - dataSource = assetDataSource; + dataSource = getAssetDataSource(); } else if (SCHEME_CONTENT.equals(scheme)) { - dataSource = contentDataSource; + dataSource = getContentDataSource(); + } else if (SCHEME_RTMP.equals(scheme)) { + dataSource = getRtmpDataSource(); } else { dataSource = baseDataSource; } @@ -144,4 +158,48 @@ public final class DefaultDataSource implements DataSource { } } + private DataSource getFileDataSource() { + if (fileDataSource == null) { + fileDataSource = new FileDataSource(listener); + } + return fileDataSource; + } + + private DataSource getAssetDataSource() { + if (assetDataSource == null) { + assetDataSource = new AssetDataSource(context, listener); + } + return assetDataSource; + } + + private DataSource getContentDataSource() { + if (contentDataSource == null) { + contentDataSource = new ContentDataSource(context, listener); + } + return contentDataSource; + } + + private DataSource getRtmpDataSource() { + if (rtmpDataSource == null) { + try { + Class clazz = Class.forName("com.google.android.exoplayer2.ext.rtmp.RtmpDataSource"); + rtmpDataSource = (DataSource) clazz.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException e) { + Log.w(TAG, "Attempting to play RTMP stream without depending on the RTMP extension"); + } catch (InstantiationException e) { + Log.e(TAG, "Error instantiating RtmpDataSource", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "Error instantiating RtmpDataSource", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Error instantiating RtmpDataSource", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Error instantiating RtmpDataSource", e); + } + if (rtmpDataSource == null) { + rtmpDataSource = baseDataSource; + } + } + return rtmpDataSource; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 1bdebf7c17..02ccfafa89 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -119,17 +119,23 @@ public final class Loader implements LoaderErrorThrower { } + /** + * A callback to be notified when a {@link Loader} has finished being released. + */ + public interface ReleaseCallback { + + /** + * Called when the {@link Loader} has finished being released. + */ + void onLoaderReleased(); + + } + public static final int RETRY = 0; public static final int RETRY_RESET_ERROR_COUNT = 1; public static final int DONT_RETRY = 2; public static final int DONT_RETRY_FATAL = 3; - private static final int MSG_START = 0; - private static final int MSG_CANCEL = 1; - private static final int MSG_END_OF_SOURCE = 2; - private static final int MSG_IO_EXCEPTION = 3; - private static final int MSG_FATAL_ERROR = 4; - private final ExecutorService downloadExecutorService; private LoadTask currentTask; @@ -150,7 +156,7 @@ public final class Loader implements LoaderErrorThrower { * * @param The type of the loadable. * @param loadable The {@link Loadable} to load. - * @param callback A callback to called when the load ends. + * @param callback A callback to be called when the load ends. * @param defaultMinRetryCount The minimum number of times the load must be retried before * {@link #maybeThrowError()} will propagate an error. * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. @@ -188,20 +194,28 @@ public final class Loader implements LoaderErrorThrower { } /** - * Releases the {@link Loader}, running {@code postLoadAction} on its thread. This method should - * be called when the {@link Loader} is no longer required. + * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer + * required. * - * @param postLoadAction A {@link Runnable} to run on the loader's thread when - * {@link Loadable#load()} is no longer running. + * @param callback A callback to be called when the release ends. Will be called synchronously + * from this method if no load is in progress, or asynchronously once the load has been + * canceled otherwise. May be null. + * @return True if {@code callback} was called synchronously. False if it will be called + * asynchronously or if {@code callback} is null. */ - public void release(Runnable postLoadAction) { + public boolean release(ReleaseCallback callback) { + boolean callbackInvoked = false; if (currentTask != null) { currentTask.cancel(true); - } - if (postLoadAction != null) { - downloadExecutorService.execute(postLoadAction); + if (callback != null) { + downloadExecutorService.execute(new ReleaseTask(callback)); + } + } else if (callback != null) { + callback.onLoaderReleased(); + callbackInvoked = true; } downloadExecutorService.shutdown(); + return callbackInvoked; } // LoaderErrorThrower implementation. @@ -228,6 +242,12 @@ public final class Loader implements LoaderErrorThrower { private static final String TAG = "LoadTask"; + private static final int MSG_START = 0; + private static final int MSG_CANCEL = 1; + private static final int MSG_END_OF_SOURCE = 2; + private static final int MSG_IO_EXCEPTION = 3; + private static final int MSG_FATAL_ERROR = 4; + private final T loadable; private final Loader.Callback callback; public final int defaultMinRetryCount; @@ -390,4 +410,24 @@ public final class Loader implements LoaderErrorThrower { } + private static final class ReleaseTask extends Handler implements Runnable { + + private final ReleaseCallback callback; + + public ReleaseTask(ReleaseCallback callback) { + this.callback = callback; + } + + @Override + public void run() { + sendEmptyMessage(0); + } + + @Override + public void handleMessage(Message msg) { + callback.onLoaderReleased(); + } + + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index bb1f88e5ea..cf2dedbe54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -1,16 +1,16 @@ /* - * 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, + * 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 + * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.exoplayer2.upstream.cache; @@ -22,28 +22,36 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; import java.util.NavigableSet; /** * Caching related utility methods. */ +@SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public final class CacheUtil { - /** Holds the counters used during caching. */ + /** Counters used during caching. */ public static class CachingCounters { - /** Total number of already cached bytes. */ - public long alreadyCachedBytes; + /** The number of bytes already in the cache. */ + public volatile long alreadyCachedBytes; + /** The number of newly cached bytes. */ + public volatile long newlyCachedBytes; + /** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */ + public volatile long contentLength = C.LENGTH_UNSET; + /** - * Total number of downloaded bytes. - * - *

    {@link #getCached(DataSpec, Cache, CachingCounters)} sets it to the count of the missing - * bytes or to {@link C#LENGTH_UNSET} if {@code dataSpec} is unbounded and content length isn't - * available in the {@code cache}. + * Returns the sum of {@link #alreadyCachedBytes} and {@link #newlyCachedBytes}. */ - public long downloadedBytes; + public long totalCachedBytes() { + return alreadyCachedBytes + newlyCachedBytes; + } } + /** Default buffer size to be used while caching. */ + public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024; + /** * Generates a cache key out of the given {@link Uri}. * @@ -64,26 +72,57 @@ public final class CacheUtil { } /** - * Returns already cached and missing bytes in the {@code cache} for the data defined by {@code - * dataSpec}. + * Sets a {@link CachingCounters} to contain the number of bytes already downloaded and the + * length for the content defined by a {@code dataSpec}. {@link CachingCounters#newlyCachedBytes} + * is reset to 0. * * @param dataSpec Defines the data to be checked. * @param cache A {@link Cache} which has the data. - * @param counters The counters to be set. If null a new {@link CachingCounters} is created and - * used. - * @return The used {@link CachingCounters} instance. + * @param counters The {@link CachingCounters} to update. */ - public static CachingCounters getCached(DataSpec dataSpec, Cache cache, - CachingCounters counters) { - try { - return internalCache(dataSpec, cache, null, null, null, 0, counters); - } catch (IOException | InterruptedException e) { - throw new IllegalStateException(e); + public static void getCached(DataSpec dataSpec, Cache cache, CachingCounters counters) { + String key = getKey(dataSpec); + long start = dataSpec.absoluteStreamPosition; + long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); + counters.contentLength = left; + counters.alreadyCachedBytes = 0; + counters.newlyCachedBytes = 0; + while (left != 0) { + long blockLength = cache.getCachedBytes(key, start, + left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); + if (blockLength > 0) { + counters.alreadyCachedBytes += blockLength; + } else { + blockLength = -blockLength; + if (blockLength == Long.MAX_VALUE) { + return; + } + } + start += blockLength; + left -= left == C.LENGTH_UNSET ? 0 : blockLength; } } /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. + * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early + * if the end of the input is reached. + * + * @param dataSpec Defines the data to be cached. + * @param cache A {@link Cache} to store the data. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param counters Counters to update during caching. + * @throws IOException If an error occurs reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public static void cache(DataSpec dataSpec, Cache cache, DataSource upstream, + CachingCounters counters) throws IOException, InterruptedException { + cache(dataSpec, cache, new CacheDataSource(cache, upstream), + new byte[DEFAULT_BUFFER_SIZE_BYTES], null, 0, counters, false); + } + + /** + * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops + * early if end of input is reached and {@code enableEOFException} is false. * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. @@ -92,123 +131,109 @@ public final class CacheUtil { * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. Used with {@code priorityTaskManager}. - * @param counters The counters to be set during caching. If not null its values reset to - * zero before using. If null a new {@link CachingCounters} is created and used. - * @return The used {@link CachingCounters} instance. + * @param counters Counters to update during caching. + * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been + * reached unexpectedly. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted. */ - public static CachingCounters cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, + public static void cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters) throws IOException, InterruptedException { + CachingCounters counters, boolean enableEOFException) + throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); - return internalCache(dataSpec, cache, dataSource, buffer, priorityTaskManager, priority, - counters); - } - /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. If {@code - * dataSource} or {@code buffer} is null performs a dry run. - * - * @param dataSpec Defines the data to be cached. - * @param cache A {@link Cache} to store the data. - * @param dataSource A {@link CacheDataSource} that works on the {@code cache}. If null a dry run - * is performed. - * @param buffer The buffer to be used while caching. If null a dry run is performed. - * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with - * caching. - * @param priority The priority of this task. Used with {@code priorityTaskManager}. - * @param counters The counters to be set during caching. If not null its values reset to - * zero before using. If null a new {@link CachingCounters} is created and used. - * @return The used {@link CachingCounters} instance. - * @throws IOException If not dry run and an error occurs reading from the source. - * @throws InterruptedException If not dry run and the thread was interrupted. - */ - private static CachingCounters internalCache(DataSpec dataSpec, Cache cache, - CacheDataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, - int priority, CachingCounters counters) throws IOException, InterruptedException { - long start = dataSpec.position; - long left = dataSpec.length; - String key = getKey(dataSpec); - if (left == C.LENGTH_UNSET) { - left = cache.getContentLength(key); - if (left == C.LENGTH_UNSET) { - left = Long.MAX_VALUE; - } - } - if (counters == null) { - counters = new CachingCounters(); + if (counters != null) { + // Initialize the CachingCounter values. + getCached(dataSpec, cache, counters); } else { - counters.alreadyCachedBytes = 0; - counters.downloadedBytes = 0; + // Dummy CachingCounters. No need to initialize as they will not be visible to the caller. + counters = new CachingCounters(); } - while (left > 0) { - long blockLength = cache.getCachedBytes(key, start, left); - // Skip already cached data + + String key = getKey(dataSpec); + long start = dataSpec.absoluteStreamPosition; + long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); + while (left != 0) { + long blockLength = cache.getCachedBytes(key, start, + left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); if (blockLength > 0) { - counters.alreadyCachedBytes += blockLength; + // Skip already cached data. } else { // There is a hole in the cache which is at least "-blockLength" long. blockLength = -blockLength; - if (dataSource != null && buffer != null) { - DataSpec subDataSpec = new DataSpec(dataSpec.uri, start, - blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength, key); - long read = readAndDiscard(subDataSpec, dataSource, buffer, priorityTaskManager, - priority); - counters.downloadedBytes += read; - if (read < blockLength) { - // Reached end of data. - break; + long read = readAndDiscard(dataSpec, start, blockLength, dataSource, buffer, + priorityTaskManager, priority, counters); + if (read < blockLength) { + // Reached to the end of the data. + if (enableEOFException && left != C.LENGTH_UNSET) { + throw new EOFException(); } - } else if (blockLength == Long.MAX_VALUE) { - counters.downloadedBytes = C.LENGTH_UNSET; break; - } else { - counters.downloadedBytes += blockLength; } } start += blockLength; - if (left != Long.MAX_VALUE) { - left -= blockLength; - } + left -= left == C.LENGTH_UNSET ? 0 : blockLength; } - return counters; } /** * Reads and discards all data specified by the {@code dataSpec}. * - * @param dataSpec Defines the data to be read. + * @param dataSpec Defines the data to be read. {@code absoluteStreamPosition} and {@code length} + * fields are overwritten by the following parameters. + * @param absoluteStreamPosition The absolute position of the data to be read. + * @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown. * @param dataSource The {@link DataSource} to read the data from. * @param buffer The buffer to be used while downloading. * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with * caching. * @param priority The priority of this task. + * @param counters Counters to be set during reading. * @return Number of read bytes, or 0 if no data is available because the end of the opened range - * has been reached. + * has been reached. */ - private static long readAndDiscard(DataSpec dataSpec, DataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, int priority) - throws IOException, InterruptedException { + private static long readAndDiscard(DataSpec dataSpec, long absoluteStreamPosition, long length, + DataSource dataSource, byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, + CachingCounters counters) throws IOException, InterruptedException { while (true) { if (priorityTaskManager != null) { // Wait for any other thread with higher priority to finish its job. priorityTaskManager.proceed(priority); } try { - dataSource.open(dataSpec); + if (Thread.interrupted()) { + throw new InterruptedException(); + } + // Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in + // case the given length exceeds the end of input. + dataSpec = new DataSpec(dataSpec.uri, dataSpec.postBody, absoluteStreamPosition, + dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition, + C.LENGTH_UNSET, dataSpec.key, + dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + long resolvedLength = dataSource.open(dataSpec); + if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { + counters.contentLength = dataSpec.absoluteStreamPosition + resolvedLength; + } long totalRead = 0; - while (true) { + while (totalRead != length) { if (Thread.interrupted()) { throw new InterruptedException(); } - int read = dataSource.read(buffer, 0, buffer.length); + int read = dataSource.read(buffer, 0, + length != C.LENGTH_UNSET ? (int) Math.min(buffer.length, length - totalRead) + : buffer.length); if (read == C.RESULT_END_OF_INPUT) { - return totalRead; + if (counters.contentLength == C.LENGTH_UNSET) { + counters.contentLength = dataSpec.absoluteStreamPosition + totalRead; + } + break; } totalRead += read; + counters.newlyCachedBytes += read; } + return totalRead; } catch (PriorityTaskManager.PriorityTooLowException exception) { // catch and try again } finally { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index bbff7dc4a2..2da6ba759b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -110,7 +110,8 @@ public final class SimpleCache implements Cache { @Override public synchronized NavigableSet getCachedSpans(String key) { CachedContent cachedContent = index.get(key); - return cachedContent == null ? null : new TreeSet(cachedContent.getSpans()); + return cachedContent == null || cachedContent.isEmpty() ? null + : new TreeSet(cachedContent.getSpans()); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index 01f667ed86..f8d5759c2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -16,16 +16,24 @@ package com.google.android.exoplayer2.util; /** - * An interface through which system clocks can be read. The {@link SystemClock} implementation + * An interface through which system clocks can be read. The {@link #DEFAULT} implementation * must be used for all non-test cases. */ public interface Clock { /** - * Returns {@link android.os.SystemClock#elapsedRealtime}. - * - * @return Elapsed milliseconds since boot. + * Default {@link Clock} to use for all non-test cases. + */ + Clock DEFAULT = new SystemClock(); + + /** + * @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); + /** + * @see android.os.SystemClock#sleep(long) + */ + void sleep(long sleepTimeMs); + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index db1122dbe7..2d4a1ec96f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -65,6 +65,7 @@ public final class MimeTypes { public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; + public static final String TEXT_SSA = BASE_TYPE_TEXT + "/x-ssa"; public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 0456bcb879..199ceff892 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -155,6 +155,9 @@ public final class ParsableBitArray { * @return An integer whose bottom n bits hold the read data. */ public int readBits(int numBits) { + if (numBits == 0) { + return 0; + } int returnValue = 0; bitOffset += numBits; while (bitOffset > 8) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java new file mode 100644 index 0000000000..53cb051230 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java @@ -0,0 +1,90 @@ +/* + * 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.util; + +import android.support.annotation.IntDef; +import com.google.android.exoplayer2.Player; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Util class for repeat mode handling. + */ +public final class RepeatModeUtil { + + /** + * Set of repeat toggle modes. Can be combined using bit-wise operations. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {REPEAT_TOGGLE_MODE_NONE, REPEAT_TOGGLE_MODE_ONE, + REPEAT_TOGGLE_MODE_ALL}) + public @interface RepeatToggleModes {} + /** + * All repeat mode buttons disabled. + */ + public static final int REPEAT_TOGGLE_MODE_NONE = 0; + /** + * "Repeat One" button enabled. + */ + public static final int REPEAT_TOGGLE_MODE_ONE = 1; + /** + * "Repeat All" button enabled. + */ + public static final int REPEAT_TOGGLE_MODE_ALL = 2; + + private RepeatModeUtil() { + // Prevent instantiation. + } + + /** + * Gets the next repeat mode out of {@code enabledModes} starting from {@code currentMode}. + * + * @param currentMode The current repeat mode. + * @param enabledModes Bitmask of enabled modes. + * @return The next repeat mode. + */ + public static @Player.RepeatMode int getNextRepeatMode(@Player.RepeatMode int currentMode, + int enabledModes) { + for (int offset = 1; offset <= 2; offset++) { + @Player.RepeatMode int proposedMode = (currentMode + offset) % 3; + if (isRepeatModeEnabled(proposedMode, enabledModes)) { + return proposedMode; + } + } + return currentMode; + } + + /** + * Verifies whether a given {@code repeatMode} is enabled in the bitmask {@code enabledModes}. + * + * @param repeatMode The mode to check. + * @param enabledModes The bitmask representing the enabled modes. + * @return {@code true} if enabled. + */ + public static boolean isRepeatModeEnabled(@Player.RepeatMode int repeatMode, int enabledModes) { + switch (repeatMode) { + case Player.REPEAT_MODE_OFF: + return true; + case Player.REPEAT_MODE_ONE: + return (enabledModes & REPEAT_TOGGLE_MODE_ONE) != 0; + case Player.REPEAT_MODE_ALL: + return (enabledModes & REPEAT_TOGGLE_MODE_ALL) != 0; + default: + return false; + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index b05675f647..1f937b721b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -18,11 +18,16 @@ package com.google.android.exoplayer2.util; /** * The standard implementation of {@link Clock}. */ -public final class SystemClock implements Clock { +/* package */ final class SystemClock implements Clock { @Override public long elapsedRealtime() { return android.os.SystemClock.elapsedRealtime(); } + @Override + public void sleep(long sleepTimeMs) { + android.os.SystemClock.sleep(sleepTimeMs); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 50932cdf48..b958a54244 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -680,25 +679,6 @@ public final class Util { return intArray; } - /** - * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec} - * that represents the remainder of the data. - * - * @param dataSpec The original {@link DataSpec}. - * @param bytesLoaded The number of bytes already loaded. - * @return A {@link DataSpec} that represents the remainder of the data. - */ - public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) { - if (bytesLoaded == 0) { - return dataSpec; - } else { - long remainingLength = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET - : dataSpec.length - bytesLoaded; - return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength, - dataSpec.key, dataSpec.flags); - } - } - /** * Returns the integer equal to the big-endian concatenation of the characters in {@code string} * as bytes. The string must be no more than four characters long. @@ -816,6 +796,85 @@ public final class Util { } } + /** + * Returns the {@link C.AudioUsage} corresponding to the specified {@link C.StreamType}. + */ + @C.AudioUsage + public static int getAudioUsageForStreamType(@C.StreamType int streamType) { + switch (streamType) { + case C.STREAM_TYPE_ALARM: + return C.USAGE_ALARM; + case C.STREAM_TYPE_DTMF: + return C.USAGE_VOICE_COMMUNICATION_SIGNALLING; + case C.STREAM_TYPE_NOTIFICATION: + return C.USAGE_NOTIFICATION; + case C.STREAM_TYPE_RING: + return C.USAGE_NOTIFICATION_RINGTONE; + case C.STREAM_TYPE_SYSTEM: + return C.USAGE_ASSISTANCE_SONIFICATION; + case C.STREAM_TYPE_VOICE_CALL: + return C.USAGE_VOICE_COMMUNICATION; + case C.STREAM_TYPE_USE_DEFAULT: + case C.STREAM_TYPE_MUSIC: + default: + return C.USAGE_MEDIA; + } + } + + /** + * Returns the {@link C.AudioContentType} corresponding to the specified {@link C.StreamType}. + */ + @C.AudioContentType + public static int getAudioContentTypeForStreamType(@C.StreamType int streamType) { + switch (streamType) { + case C.STREAM_TYPE_ALARM: + case C.STREAM_TYPE_DTMF: + case C.STREAM_TYPE_NOTIFICATION: + case C.STREAM_TYPE_RING: + case C.STREAM_TYPE_SYSTEM: + return C.CONTENT_TYPE_SONIFICATION; + case C.STREAM_TYPE_VOICE_CALL: + return C.CONTENT_TYPE_SPEECH; + case C.STREAM_TYPE_USE_DEFAULT: + case C.STREAM_TYPE_MUSIC: + default: + return C.CONTENT_TYPE_MUSIC; + } + } + + /** + * Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}. + */ + @C.StreamType + public static int getStreamTypeForAudioUsage(@C.AudioUsage int usage) { + switch (usage) { + case C.USAGE_MEDIA: + case C.USAGE_GAME: + case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return C.STREAM_TYPE_MUSIC; + case C.USAGE_ASSISTANCE_SONIFICATION: + return C.STREAM_TYPE_SYSTEM; + case C.USAGE_VOICE_COMMUNICATION: + return C.STREAM_TYPE_VOICE_CALL; + case C.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return C.STREAM_TYPE_DTMF; + case C.USAGE_ALARM: + return C.STREAM_TYPE_ALARM; + case C.USAGE_NOTIFICATION_RINGTONE: + return C.STREAM_TYPE_RING; + case C.USAGE_NOTIFICATION: + case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case C.USAGE_NOTIFICATION_EVENT: + return C.STREAM_TYPE_NOTIFICATION; + case C.USAGE_ASSISTANCE_ACCESSIBILITY: + case C.USAGE_UNKNOWN: + default: + return C.STREAM_TYPE_DEFAULT; + } + } + /** * Makes a best guess to infer the type from a {@link Uri}. * @@ -836,7 +895,7 @@ public final class Util { */ @C.ContentType public static int inferContentType(String fileName) { - fileName = fileName.toLowerCase(); + fileName = Util.toLowerInvariant(fileName); if (fileName.endsWith(".mpd")) { return C.TYPE_DASH; } else if (fileName.endsWith(".m3u8")) { @@ -977,15 +1036,15 @@ public final class Util { int expectedLength = length - percentCharacterCount * 2; StringBuilder builder = new StringBuilder(expectedLength); Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName); - int endOfLastMatch = 0; + int startOfNotEscaped = 0; while (percentCharacterCount > 0 && matcher.find()) { char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16); - builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter); - endOfLastMatch = matcher.end(); + builder.append(fileName, startOfNotEscaped, matcher.start()).append(unescapedCharacter); + startOfNotEscaped = matcher.end(); percentCharacterCount--; } - if (endOfLastMatch < length) { - builder.append(fileName, endOfLastMatch, length); + if (startOfNotEscaped < length) { + builder.append(fileName, startOfNotEscaped, length); } if (builder.length() != expectedLength) { return null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 23d9941cf3..e32f23fed7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -41,6 +41,8 @@ import static android.opengl.GLES20.glDeleteTextures; import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -68,19 +70,8 @@ public final class DummySurface extends Surface { private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - /** - * Whether the device supports secure dummy surfaces. - */ - public static final boolean SECURE_SUPPORTED; - static { - if (Util.SDK_INT >= 17) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - SECURE_SUPPORTED = extensions != null && extensions.contains("EGL_EXT_protected_content"); - } else { - SECURE_SUPPORTED = false; - } - } + private static boolean secureSupported; + private static boolean secureSupportedInitialized; /** * Whether the surface is secure. @@ -90,18 +81,40 @@ public final class DummySurface extends Surface { private final DummySurfaceThread thread; private boolean threadReleased; + /** + * Returns whether the device supports secure dummy surfaces. + * + * @param context Any {@link Context}. + * @return Whether the device supports secure dummy surfaces. + */ + public static synchronized boolean isSecureSupported(Context context) { + if (!secureSupportedInitialized) { + if (Util.SDK_INT >= 17) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + secureSupported = extensions != null && extensions.contains("EGL_EXT_protected_content") + && !deviceNeedsSecureDummySurfaceWorkaround(context); + } + secureSupportedInitialized = true; + } + return secureSupported; + } + /** * Returns a newly created dummy surface. The surface must be released by calling {@link #release} * when it's no longer required. *

    * Must only be called if {@link Util#SDK_INT} is 17 or higher. * + * @param context Any {@link Context}. * @param secure Whether a secure surface is required. Must only be requested if - * {@link #SECURE_SUPPORTED} is {@code true}. + * {@link #isSecureSupported(Context)} returns {@code true}. + * @throws IllegalStateException If a secure surface is requested on a device for which + * {@link #isSecureSupported(Context)} returns {@code false}. */ - public static DummySurface newInstanceV17(boolean secure) { + public static DummySurface newInstanceV17(Context context, boolean secure) { assertApiLevel17OrHigher(); - Assertions.checkState(!secure || SECURE_SUPPORTED); + Assertions.checkState(!secure || isSecureSupported(context)); DummySurfaceThread thread = new DummySurfaceThread(); return thread.init(secure); } @@ -133,6 +146,23 @@ public final class DummySurface extends Surface { } } + /** + * Returns whether the device is known to advertise secure surface textures but not implement them + * correctly. + * + * @param context Any {@link Context}. + */ + private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { + return Util.SDK_INT == 24 + && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955")) + && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager()); + } + + @TargetApi(24) + private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + } + private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, Callback { @@ -255,8 +285,8 @@ public final class DummySurface extends Surface { if (secure) { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_PROTECTED_CONTENT_EXT, - EGL_TRUE, EGL_NONE}; + EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, + EGL_NONE}; } else { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index fbbcd9a99a..9a2927cc3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; @@ -62,16 +63,23 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] { 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480}; + // Generally there is zero or one pending output stream offset. We track more offsets to allow for + // pending output streams that have fewer frames than the codec latency. + private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + + private final Context context; private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; private final boolean deviceNeedsAutoFrcWorkaround; + private final long[] pendingOutputStreamOffsetsUs; private Format[] streamFormats; private CodecMaxValues codecMaxValues; private Surface surface; + private Surface dummySurface; @C.VideoScalingMode private int scalingMode; private boolean renderedFirstFrame; @@ -95,6 +103,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private int tunnelingAudioSessionId; /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; + private long outputStreamOffsetUs; + private int pendingOutputStreamOffsetCount; + /** * @param context A context. * @param mediaCodecSelector A decoder selector. @@ -157,9 +168,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; + this.context = context.getApplicationContext(); frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); + pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; + outputStreamOffsetUs = C.TIME_UNSET; joiningDeadlineMs = C.TIME_UNSET; currentWidth = Format.NO_VALUE; currentHeight = Format.NO_VALUE; @@ -219,9 +233,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onStreamChanged(Format[] formats) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { streamFormats = formats; - super.onStreamChanged(formats); + if (outputStreamOffsetUs == C.TIME_UNSET) { + outputStreamOffsetUs = offsetUs; + } else { + if (pendingOutputStreamOffsetCount == pendingOutputStreamOffsetsUs.length) { + Log.w(TAG, "Too many stream changes, so dropping offset: " + + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]); + } else { + pendingOutputStreamOffsetCount++; + } + pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1] = offsetUs; + } + super.onStreamChanged(formats, offsetUs); } @Override @@ -229,6 +254,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { super.onPositionReset(positionUs, joining); clearRenderedFirstFrame(); consecutiveDroppedFrameCount = 0; + if (pendingOutputStreamOffsetCount != 0) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[pendingOutputStreamOffsetCount - 1]; + pendingOutputStreamOffsetCount = 0; + } if (joining) { setJoiningDeadlineMs(); } else { @@ -238,7 +267,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override public boolean isReady() { - if ((renderedFirstFrame || super.shouldInitCodec()) && super.isReady()) { + if (super.isReady() && (renderedFirstFrame || (dummySurface != null && surface == dummySurface) + || getCodec() == null || tunneling)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. joiningDeadlineMs = C.TIME_UNSET; return true; @@ -275,10 +305,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { currentHeight = Format.NO_VALUE; currentPixelWidthHeightRatio = Format.NO_VALUE; pendingPixelWidthHeightRatio = Format.NO_VALUE; + outputStreamOffsetUs = C.TIME_UNSET; + pendingOutputStreamOffsetCount = 0; clearReportedVideoSize(); clearRenderedFirstFrame(); frameReleaseTimeHelper.disable(); tunnelingOnFrameRenderedListener = null; + tunneling = false; try { super.onDisabled(); } finally { @@ -303,6 +336,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void setSurface(Surface surface) throws ExoPlaybackException { + if (surface == null) { + // Use a dummy surface if possible. + if (dummySurface != null) { + surface = dummySurface; + } else { + MediaCodecInfo codecInfo = getCodecInfo(); + if (codecInfo != null && shouldUseDummySurface(codecInfo.secure)) { + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + surface = dummySurface; + } + } + } // We only need to update the codec if the surface has changed. if (this.surface != surface) { this.surface = surface; @@ -316,7 +361,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeInitCodec(); } } - if (surface != null) { + if (surface != null && surface != dummySurface) { // If we know the video size, report it again immediately. maybeRenotifyVideoSizeChanged(); // We haven't rendered to the new surface yet. @@ -329,17 +374,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { clearReportedVideoSize(); clearRenderedFirstFrame(); } - } else if (surface != null) { - // The surface is unchanged and non-null. If we know the video size and/or have already - // rendered to the surface, report these again immediately. + } else if (surface != null && surface != dummySurface) { + // The surface is set and unchanged. If we know the video size and/or have already rendered to + // the surface, report these again immediately. maybeRenotifyVideoSizeChanged(); maybeRenotifyRenderedFirstFrame(); } } @Override - protected boolean shouldInitCodec() { - return super.shouldInitCodec() && surface != null && surface.isValid(); + protected boolean shouldInitCodec(MediaCodecInfo codecInfo) { + return surface != null || shouldUseDummySurface(codecInfo.secure); } @Override @@ -348,12 +393,34 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats); MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround, tunnelingAudioSessionId); + if (surface == null) { + Assertions.checkState(shouldUseDummySurface(codecInfo.secure)); + if (dummySurface == null) { + dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + } + surface = dummySurface; + } codec.configure(mediaFormat, surface, crypto, 0); if (Util.SDK_INT >= 23 && tunneling) { tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec); } } + @Override + protected void releaseCodec() { + try { + super.releaseCodec(); + } finally { + if (dummySurface != null) { + if (surface == dummySurface) { + surface = null; + } + dummySurface.release(); + dummySurface = null; + } + } + } + @Override protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { @@ -410,23 +477,42 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Format oldFormat, Format newFormat) { return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && newFormat.maxInputSize <= codecMaxValues.inputSize; + && getMaxInputSize(newFormat) <= codecMaxValues.inputSize; } @Override protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, boolean shouldSkip) { + while (pendingOutputStreamOffsetCount != 0 + && bufferPresentationTimeUs >= pendingOutputStreamOffsetsUs[0]) { + outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; + pendingOutputStreamOffsetCount--; + System.arraycopy(pendingOutputStreamOffsetsUs, 1, pendingOutputStreamOffsetsUs, 0, + pendingOutputStreamOffsetCount); + } + long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; + if (shouldSkip) { - skipOutputBuffer(codec, bufferIndex); + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } + long earlyUs = bufferPresentationTimeUs - positionUs; + if (surface == dummySurface) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(earlyUs)) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + return true; + } + return false; + } + if (!renderedFirstFrame) { if (Util.SDK_INT >= 21) { - renderOutputBufferV21(codec, bufferIndex, System.nanoTime()); + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime()); } else { - renderOutputBuffer(codec, bufferIndex); + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); } return true; } @@ -435,9 +521,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } - // Compute how many microseconds it is until the buffer's presentation time. + // Fine-grained adjustment of earlyUs based on the elapsed time since the start of the current + // iteration of the rendering loop. long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs; - long earlyUs = bufferPresentationTimeUs - positionUs - elapsedSinceStartOfLoopUs; + earlyUs -= elapsedSinceStartOfLoopUs; // Compute the buffer's desired release time in nanoseconds. long systemTimeNs = System.nanoTime(); @@ -449,15 +536,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { - // We're more than 30ms late rendering the frame. - dropOutputBuffer(codec, bufferIndex); + dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } if (Util.SDK_INT >= 21) { // Let the underlying framework time the release. if (earlyUs < 50000) { - renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs); + renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs); return true; } } else { @@ -473,7 +559,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Thread.currentThread().interrupt(); } } - renderOutputBuffer(codec, bufferIndex); + renderOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } } @@ -491,20 +577,33 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * measured at the start of the current iteration of the rendering loop. */ protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { - // Drop the frame if we're more than 30ms late rendering the frame. - return earlyUs < -30000; + return isBufferLate(earlyUs); } - private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Skips the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to skip. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void skipOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { TraceUtil.beginSection("skipVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); + codec.releaseOutputBuffer(index, false); TraceUtil.endSection(); decoderCounters.skippedOutputBufferCount++; } - private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Drops the output buffer with the specified index. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void dropOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { TraceUtil.beginSection("dropVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); + codec.releaseOutputBuffer(index, false); TraceUtil.endSection(); decoderCounters.droppedOutputBufferCount++; droppedFrames++; @@ -516,27 +615,50 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } - private void renderOutputBuffer(MediaCodec codec, int bufferIndex) { + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is less than 21. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + */ + protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { maybeNotifyVideoSizeChanged(); TraceUtil.beginSection("releaseOutputBuffer"); - codec.releaseOutputBuffer(bufferIndex, true); + codec.releaseOutputBuffer(index, true); TraceUtil.endSection(); decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; maybeNotifyRenderedFirstFrame(); } + /** + * Renders the output buffer with the specified index. This method is only called if the platform + * API version of the device is 21 or later. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. + */ @TargetApi(21) - private void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { + protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs, + long releaseTimeNs) { maybeNotifyVideoSizeChanged(); TraceUtil.beginSection("releaseOutputBuffer"); - codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); + codec.releaseOutputBuffer(index, releaseTimeNs); TraceUtil.endSection(); decoderCounters.renderedOutputBufferCount++; consecutiveDroppedFrameCount = 0; maybeNotifyRenderedFirstFrame(); } + private boolean shouldUseDummySurface(boolean codecIsSecure) { + return Util.SDK_INT >= 23 && !tunneling + && (!codecIsSecure || DummySurface.isSecureSupported(context)); + } + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; @@ -608,6 +730,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } + private static boolean isBufferLate(long earlyUs) { + // Class a buffer as late if it should have been presented more than 30ms ago. + return earlyUs < -30000; + } + @SuppressLint("InlinedApi") private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { @@ -727,18 +854,27 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input size for a given format. + * Returns a maximum input buffer size for a given format. * * @param format The format. - * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be - * determined. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. */ private static int getMaxInputSize(Format format) { if (format.maxInputSize != Format.NO_VALUE) { - // The format defines an explicit maximum input size. - return format.maxInputSize; + // The format defines an explicit maximum input size. Add the total size of initialization + // data buffers, as they may need to be queued in the same input buffer as the largest sample. + int totalInitializationDataSize = 0; + int initializationDataCount = format.initializationData.size(); + for (int i = 0; i < initializationDataCount; i++) { + totalInitializationDataSize += format.initializationData.get(i).length; + } + return format.maxInputSize + totalInitializationDataSize; + } else { + // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of + // initialization data. + return getMaxInputSize(format.sampleMimeType, format.width, format.height); } - return getMaxInputSize(format.sampleMimeType, format.width, format.height); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 4771f2572c..ad489c2312 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.C; @TargetApi(16) public final class VideoFrameReleaseTimeHelper { + private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1; private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; private static final long MAX_ALLOWED_DRIFT_NS = 20000000; @@ -52,26 +53,25 @@ public final class VideoFrameReleaseTimeHelper { private long frameCount; /** - * Constructs an instance that smoothes frame release timestamps but does not align them with + * Constructs an instance that smooths frame release timestamps but does not align them with * the default display's vsync signal. */ public VideoFrameReleaseTimeHelper() { - this(-1 /* Value unused */, false); + this(DISPLAY_REFRESH_RATE_UNKNOWN); } /** - * Constructs an instance that smoothes frame release timestamps and aligns them with the default + * Constructs an instance that smooths frame release timestamps and aligns them with the default * display's vsync signal. * * @param context A context from which information about the default display can be retrieved. */ public VideoFrameReleaseTimeHelper(Context context) { - this(getDefaultDisplayRefreshRate(context), true); + this(getDefaultDisplayRefreshRate(context)); } - private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate, - boolean useDefaultDisplayVsync) { - this.useDefaultDisplayVsync = useDefaultDisplayVsync; + private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) { + useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; if (useDefaultDisplayVsync) { vsyncSampler = VSyncSampler.getInstance(); vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); @@ -200,9 +200,10 @@ public final class VideoFrameReleaseTimeHelper { return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; } - private static float getDefaultDisplayRefreshRate(Context context) { + private static double getDefaultDisplayRefreshRate(Context context) { WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return manager.getDefaultDisplay().getRefreshRate(); + return manager.getDefaultDisplay() != null ? manager.getDefaultDisplay().getRefreshRate() + : DISPLAY_REFRESH_RATE_UNKNOWN; } /** diff --git a/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg new file mode 100644 index 0000000000..a364587320 --- /dev/null +++ b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg @@ -0,0 +1,51 @@ + + + + Produced by OmniGraffle 7.4 + 2017-07-19 14:26:00 +0000 + + + + + + + + + + + + + + + Canvas 1 + + + Layer 1 + + + + period1 + + + + + window1 + + + + + + + time + + + + + + + + + + + + diff --git a/library/dash/build.gradle b/library/dash/build.gradle index ebad5a8603..2220e5b250 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -22,23 +23,20 @@ android { targetSdkVersion project.ext.targetSdkVersion } - sourceSets { - androidTest { - java.srcDirs += "../../testutils/src/main/java/" - } - } - buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion compile 'com.android.support:support-core-utils:' + supportLibraryVersion + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/androidTest/AndroidManifest.xml index ac2511d3bd..a9b143253f 100644 --- a/library/dash/src/androidTest/AndroidManifest.xml +++ b/library/dash/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index c9f1ca1030..bac1c272e8 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -62,7 +62,7 @@ public final class DashUtilTest extends TestCase { } private static AdaptationSet newAdaptationSets(Representation... representations) { - return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null); + return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null); } private static Representation newRepresentations(DrmInitData drmInitData) { @@ -75,7 +75,7 @@ public final class DashUtilTest extends TestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType", new byte[]{1, 4, 7, 0, 3, 6})); } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 5b8760f929..3ce4b37ec6 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -115,12 +115,12 @@ public class DashManifestParserTest extends InstrumentationTestCase { buildCea708AccessibilityDescriptors("Wrong format"))); } - private static List buildCea608AccessibilityDescriptors(String value) { - return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-608:2015", value)); + private static List buildCea608AccessibilityDescriptors(String value) { + return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } - private static List buildCea708AccessibilityDescriptors(String value) { - return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-708:2015", value)); + private static List buildCea708AccessibilityDescriptors(String value) { + return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null)); } } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index c796025b08..7d77ae82d9 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -30,8 +30,6 @@ import junit.framework.TestCase; public class DashManifestTest extends TestCase { private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", ""); - private static final List DUMMY_ACCESSIBILITY_DESCRIPTORS = - Collections.emptyList(); private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase(); private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0); @@ -190,8 +188,7 @@ public class DashManifestTest extends TestCase { } private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { - return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), - DUMMY_ACCESSIBILITY_DESCRIPTORS); + return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 72f728092c..4e25c0e333 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -28,8 +28,8 @@ public interface DashChunkSource extends ChunkSource { interface Factory { DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, boolean enableCea608Track); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 5e0541cb31..81b4a4ceed 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash; import android.util.Pair; +import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; @@ -30,13 +31,14 @@ import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.Descriptor; import com.google.android.exoplayer2.source.dash.manifest.Representation; -import com.google.android.exoplayer2.source.dash.manifest.SchemeValuePair; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -55,7 +57,7 @@ import java.util.List; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; private final TrackGroupArray trackGroups; - private final EmbeddedTrackInfo[] embeddedTrackInfos; + private final TrackGroupInfo[] trackGroupInfos; private Callback callback; private ChunkSampleStream[] sampleStreams; @@ -80,9 +82,9 @@ import java.util.List; sampleStreams = newSampleStreamArray(0); sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - Pair result = buildTrackGroups(adaptationSets); + Pair result = buildTrackGroups(adaptationSets); trackGroups = result.first; - embeddedTrackInfos = result.second; + trackGroupInfos = result.second; } public void updateManifest(DashManifest manifest, int periodIndex) { @@ -104,7 +106,7 @@ import java.util.List; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; callback.onPrepared(this); } @@ -122,7 +124,6 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - int adaptationSetCount = adaptationSets.size(); HashMap> primarySampleStreams = new HashMap<>(); // First pass for primary tracks. for (int i = 0; i < selections.length; i++) { @@ -133,14 +134,15 @@ import java.util.List; stream.release(); streams[i] = null; } else { - int adaptationSetIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - primarySampleStreams.put(adaptationSetIndex, stream); + int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); + primarySampleStreams.put(trackGroupIndex, stream); } } if (streams[i] == null && selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - if (trackGroupIndex < adaptationSetCount) { - ChunkSampleStream stream = buildSampleStream(trackGroupIndex, + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.isPrimary) { + ChunkSampleStream stream = buildSampleStream(trackGroupInfo, selections[i], positionUs); primarySampleStreams.put(trackGroupIndex, stream); streams[i] = stream; @@ -160,11 +162,10 @@ import java.util.List; // may have been replaced, selected or deselected. if (selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - if (trackGroupIndex >= adaptationSetCount) { - int embeddedTrackIndex = trackGroupIndex - adaptationSetCount; - EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex]; - int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex; - ChunkSampleStream primaryStream = primarySampleStreams.get(adaptationSetIndex); + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (!trackGroupInfo.isPrimary) { + ChunkSampleStream primaryStream = primarySampleStreams.get( + trackGroupInfo.primaryTrackGroupIndex); SampleStream stream = streams[i]; boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream : (stream instanceof EmbeddedSampleStream @@ -172,7 +173,7 @@ import java.util.List; if (!mayRetainStream) { releaseIfEmbeddedSampleStream(stream); streams[i] = primaryStream == null ? new EmptySampleStream() - : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType); + : primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); streamResetFlags[i] = true; } } @@ -187,7 +188,7 @@ import java.util.List; @Override public void discardBuffer(long positionUs) { for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.discardUnselectedEmbeddedTracksTo(positionUs); + sampleStream.discardEmbeddedTracksTo(positionUs); } } @@ -208,14 +209,7 @@ import java.util.List; @Override public long getBufferedPositionUs() { - long bufferedPositionUs = Long.MAX_VALUE; - for (ChunkSampleStream sampleStream : sampleStreams) { - long rendererBufferedPositionUs = sampleStream.getBufferedPositionUs(); - if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } - return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + return sequenceableLoader.getBufferedPositionUs(); } @Override @@ -235,49 +229,114 @@ import java.util.List; // Internal methods. - private static Pair buildTrackGroups( + private static Pair buildTrackGroups( List adaptationSets) { - int adaptationSetCount = adaptationSets.size(); - int embeddedTrackCount = getEmbeddedTrackCount(adaptationSets); - TrackGroup[] trackGroupArray = new TrackGroup[adaptationSetCount + embeddedTrackCount]; - EmbeddedTrackInfo[] embeddedTrackInfos = new EmbeddedTrackInfo[embeddedTrackCount]; + int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); - int embeddedTrackIndex = 0; - for (int i = 0; i < adaptationSetCount; i++) { - AdaptationSet adaptationSet = adaptationSets.get(i); - List representations = adaptationSet.representations; + int primaryGroupCount = groupedAdaptationSetIndices.length; + boolean[] primaryGroupHasEventMessageTrackFlags = new boolean[primaryGroupCount]; + boolean[] primaryGroupHasCea608TrackFlags = new boolean[primaryGroupCount]; + int totalGroupCount = primaryGroupCount; + for (int i = 0; i < primaryGroupCount; i++) { + if (hasEventMessageTrack(adaptationSets, groupedAdaptationSetIndices[i])) { + primaryGroupHasEventMessageTrackFlags[i] = true; + totalGroupCount++; + } + if (hasCea608Track(adaptationSets, groupedAdaptationSetIndices[i])) { + primaryGroupHasCea608TrackFlags[i] = true; + totalGroupCount++; + } + } + + TrackGroup[] trackGroups = new TrackGroup[totalGroupCount]; + TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[totalGroupCount]; + + int trackGroupCount = 0; + for (int i = 0; i < primaryGroupCount; i++) { + int[] adaptationSetIndices = groupedAdaptationSetIndices[i]; + List representations = new ArrayList<>(); + for (int adaptationSetIndex : adaptationSetIndices) { + representations.addAll(adaptationSets.get(adaptationSetIndex).representations); + } Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { formats[j] = representations.get(j).format; } - trackGroupArray[i] = new TrackGroup(formats); - if (hasEventMessageTrack(adaptationSet)) { - Format format = Format.createSampleFormat(adaptationSet.id + ":emsg", + + AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); + int primaryTrackGroupIndex = trackGroupCount; + boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i]; + boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i]; + + trackGroups[trackGroupCount] = new TrackGroup(formats); + trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(firstAdaptationSet.type, + adaptationSetIndices, primaryTrackGroupIndex, true, hasEventMessageTrack, hasCea608Track); + if (hasEventMessageTrack) { + Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); - trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); - embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_METADATA); + trackGroups[trackGroupCount] = new TrackGroup(format); + trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_METADATA, + adaptationSetIndices, primaryTrackGroupIndex, false, false, false); } - if (hasCea608Track(adaptationSet)) { - Format format = Format.createTextSampleFormat(adaptationSet.id + ":cea608", - MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null); - trackGroupArray[adaptationSetCount + embeddedTrackIndex] = new TrackGroup(format); - embeddedTrackInfos[embeddedTrackIndex++] = new EmbeddedTrackInfo(i, C.TRACK_TYPE_TEXT); + if (hasCea608Track) { + Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608", + MimeTypes.APPLICATION_CEA608, 0, null); + trackGroups[trackGroupCount] = new TrackGroup(format); + trackGroupInfos[trackGroupCount++] = new TrackGroupInfo(C.TRACK_TYPE_TEXT, + adaptationSetIndices, primaryTrackGroupIndex, false, false, false); } } - return Pair.create(new TrackGroupArray(trackGroupArray), embeddedTrackInfos); + return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos); } - private ChunkSampleStream buildSampleStream(int adaptationSetIndex, + private static int[][] getGroupedAdaptationSetIndices(List adaptationSets) { + int adaptationSetCount = adaptationSets.size(); + SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount); + for (int i = 0; i < adaptationSetCount; i++) { + idToIndexMap.put(adaptationSets.get(i).id, i); + } + + int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][]; + boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount]; + + int groupCount = 0; + for (int i = 0; i < adaptationSetCount; i++) { + if (adaptationSetUsedFlags[i]) { + // This adaptation set has already been included in a group. + continue; + } + adaptationSetUsedFlags[i] = true; + Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty( + adaptationSets.get(i).supplementalProperties); + if (adaptationSetSwitchingProperty == null) { + groupedAdaptationSetIndices[groupCount++] = new int[] {i}; + } else { + String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(","); + int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; + adaptationSetIndices[0] = i; + for (int j = 0; j < extraAdaptationSetIds.length; j++) { + int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j])); + adaptationSetUsedFlags[extraIndex] = true; + adaptationSetIndices[1 + j] = extraIndex; + } + groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; + } + } + + return groupCount < adaptationSetCount + ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices; + } + + private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo, TrackSelection selection, long positionUs) { - AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); int embeddedTrackCount = 0; int[] embeddedTrackTypes = new int[2]; - boolean enableEventMessageTrack = hasEventMessageTrack(adaptationSet); + boolean enableEventMessageTrack = trackGroupInfo.hasEmbeddedEventMessageTrack; if (enableEventMessageTrack) { embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA; } - boolean enableCea608Track = hasCea608Track(adaptationSet); + boolean enableCea608Track = trackGroupInfo.hasEmbeddedCea608Track; if (enableCea608Track) { embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT; } @@ -285,45 +344,48 @@ import java.util.List; embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); } DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( - manifestLoaderErrorThrower, manifest, periodIndex, adaptationSetIndex, selection, - elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); - ChunkSampleStream stream = new ChunkSampleStream<>(adaptationSet.type, + manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices, + selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack, + enableCea608Track); + ChunkSampleStream stream = new ChunkSampleStream<>(trackGroupInfo.trackType, embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); return stream; } - private static int getEmbeddedTrackCount(List adaptationSets) { - int embeddedTrackCount = 0; - for (int i = 0; i < adaptationSets.size(); i++) { - AdaptationSet adaptationSet = adaptationSets.get(i); - if (hasEventMessageTrack(adaptationSet)) { - embeddedTrackCount++; - } - if (hasCea608Track(adaptationSet)) { - embeddedTrackCount++; + private static Descriptor findAdaptationSetSwitchingProperty(List descriptors) { + for (int i = 0; i < descriptors.size(); i++) { + Descriptor descriptor = descriptors.get(i); + if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) { + return descriptor; } } - return embeddedTrackCount; + return null; } - private static boolean hasEventMessageTrack(AdaptationSet adaptationSet) { - List representations = adaptationSet.representations; - for (int i = 0; i < representations.size(); i++) { - Representation representation = representations.get(i); - if (!representation.inbandEventStreams.isEmpty()) { - return true; + private static boolean hasEventMessageTrack(List adaptationSets, + int[] adaptationSetIndices) { + for (int i : adaptationSetIndices) { + List representations = adaptationSets.get(i).representations; + for (int j = 0; j < representations.size(); j++) { + Representation representation = representations.get(j); + if (!representation.inbandEventStreams.isEmpty()) { + return true; + } } } return false; } - private static boolean hasCea608Track(AdaptationSet adaptationSet) { - List descriptors = adaptationSet.accessibilityDescriptors; - for (int i = 0; i < descriptors.size(); i++) { - SchemeValuePair descriptor = descriptors.get(i); - if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { - return true; + private static boolean hasCea608Track(List adaptationSets, + int[] adaptationSetIndices) { + for (int i : adaptationSetIndices) { + List descriptors = adaptationSets.get(i).accessibilityDescriptors; + for (int j = 0; j < descriptors.size(); j++) { + Descriptor descriptor = descriptors.get(j); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)) { + return true; + } } } return false; @@ -340,14 +402,24 @@ import java.util.List; } } - private static final class EmbeddedTrackInfo { + private static final class TrackGroupInfo { - public final int adaptationSetIndex; + public final int[] adaptationSetIndices; public final int trackType; + public final boolean isPrimary; - public EmbeddedTrackInfo(int adaptationSetIndex, int trackType) { - this.adaptationSetIndex = adaptationSetIndex; + public final int primaryTrackGroupIndex; + public final boolean hasEmbeddedEventMessageTrack; + public final boolean hasEmbeddedCea608Track; + + public TrackGroupInfo(int trackType, int[] adaptationSetIndices, int primaryTrackGroupIndex, + boolean isPrimary, boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track) { this.trackType = trackType; + this.adaptationSetIndices = adaptationSetIndices; + this.primaryTrackGroupIndex = primaryTrackGroupIndex; + this.isPrimary = isPrimary; + this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack; + this.hasEmbeddedCea608Track = hasEmbeddedCea608Track; } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index cd995f3739..315e87dcd3 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -22,6 +22,7 @@ import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; @@ -52,6 +53,10 @@ import java.util.TimeZone; */ public final class DashMediaSource implements MediaSource { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -281,7 +286,8 @@ public final class DashMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int periodIndex, Allocator allocator, long positionUs) { + public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) { + int periodIndex = periodId.periodIndex; EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs( manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, @@ -627,9 +633,9 @@ public final class DashMediaSource implements MediaSource { private final long windowDefaultStartPositionUs; private final DashManifest manifest; - public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs, - int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs, - long windowDefaultStartPositionUs, DashManifest manifest) { + public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs, int firstPeriodId, + long offsetInFirstPeriodUs, long windowDurationUs, long windowDefaultStartPositionUs, + DashManifest manifest) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.firstPeriodId = firstPeriodId; @@ -652,7 +658,7 @@ public final class DashMediaSource implements MediaSource { + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) - - offsetInFirstPeriodUs, false); + - offsetInFirstPeriodUs); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index e679ef635c..297052f65a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** @@ -69,20 +70,21 @@ public class DefaultDashChunkSource implements DashChunkSource { @Override public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int adaptationSetIndex, - TrackSelection trackSelection, long elapsedRealtimeOffsetMs, + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, boolean enableCea608Track) { DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, - adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs, + adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs, maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); } } private final LoaderErrorThrower manifestLoaderErrorThrower; - private final int adaptationSetIndex; + private final int[] adaptationSetIndices; private final TrackSelection trackSelection; + private final int trackType; private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; @@ -98,8 +100,9 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifest The initial manifest. * @param periodIndex The index of the period in the manifest. - * @param adaptationSetIndex The index of the adaptation set in the period. + * @param adaptationSetIndices The indices of the adaptation sets in the period. * @param trackSelection The track selection. + * @param trackType The type of the tracks in the selection. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified @@ -112,26 +115,27 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, - boolean enableEventMessageTrack, boolean enableCea608Track) { + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int trackType, DataSource dataSource, + long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack, + boolean enableCea608Track) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; - this.adaptationSetIndex = adaptationSetIndex; + this.adaptationSetIndices = adaptationSetIndices; this.trackSelection = trackSelection; + this.trackType = trackType; this.dataSource = dataSource; this.periodIndex = periodIndex; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - AdaptationSet adaptationSet = getAdaptationSet(); - List representations = adaptationSet.representations; + List representations = getRepresentations(); representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, - enableEventMessageTrack, enableCea608Track, adaptationSet.type); + enableEventMessageTrack, enableCea608Track); } } @@ -141,7 +145,7 @@ public class DefaultDashChunkSource implements DashChunkSource { manifest = newManifest; periodIndex = newPeriodIndex; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getAdaptationSet().representations; + List representations = getRepresentations(); for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); @@ -248,9 +252,9 @@ public class DefaultDashChunkSource implements DashChunkSource { } int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); - out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum, - maxSegmentCount); + out.chunk = newMediaChunk(representationHolder, dataSource, trackType, + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), segmentNum, maxSegmentCount); } @Override @@ -298,8 +302,13 @@ public class DefaultDashChunkSource implements DashChunkSource { // Private methods. - private AdaptationSet getAdaptationSet() { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex); + private ArrayList getRepresentations() { + List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; + ArrayList representations = new ArrayList<>(); + for (int adaptationSetIndex : adaptationSetIndices) { + representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations); + } + return representations; } private long getNowUnixTimeUs() { @@ -332,7 +341,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } private static Chunk newMediaChunk(RepresentationHolder representationHolder, - DataSource dataSource, Format trackFormat, int trackSelectionReason, + DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); @@ -343,8 +352,7 @@ public class DefaultDashChunkSource implements DashChunkSource { DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), segmentUri.start, segmentUri.length, representation.getCacheKey()); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, - representationHolder.trackType, trackFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat); } else { int segmentCount = 1; for (int i = 1; i < maxSegmentCount; i++) { @@ -371,7 +379,6 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { - public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; @@ -381,10 +388,9 @@ public class DefaultDashChunkSource implements DashChunkSource { private int segmentNumShift; public RepresentationHolder(long periodDurationUs, Representation representation, - boolean enableEventMessageTrack, boolean enableCea608Track, int trackType) { + boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; - this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index 097676b89f..fd91a2f784 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -41,31 +41,40 @@ public class AdaptationSet { public final int type; /** - * The {@link Representation}s in the adaptation set. + * {@link Representation}s in the adaptation set. */ public final List representations; /** - * The accessibility descriptors in the adaptation set. + * Accessibility descriptors in the adaptation set. */ - public final List accessibilityDescriptors; + public final List accessibilityDescriptors; + + /** + * Supplemental properties in the adaptation set. + */ + public final List supplementalProperties; /** * @param id A non-negative identifier for the adaptation set that's unique in the scope of its * containing period, or {@link #ID_UNSET} if not specified. * @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C} * {@code TRACK_TYPE_*} constants. - * @param representations The {@link Representation}s in the adaptation set. - * @param accessibilityDescriptors The accessibility descriptors in the adaptation set. + * @param representations {@link Representation}s in the adaptation set. + * @param accessibilityDescriptors Accessibility descriptors in the adaptation set. + * @param supplementalProperties Supplemental properties in the adaptation set. */ public AdaptationSet(int id, int type, List representations, - List accessibilityDescriptors) { + List accessibilityDescriptors, List supplementalProperties) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); this.accessibilityDescriptors = accessibilityDescriptors == null - ? Collections.emptyList() + ? Collections.emptyList() : Collections.unmodifiableList(accessibilityDescriptors); + this.supplementalProperties = supplementalProperties == null + ? Collections.emptyList() + : Collections.unmodifiableList(supplementalProperties); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index eb51c8312d..cd02e27fce 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -134,7 +134,8 @@ public class DashManifest { } while(key.periodIndex == periodIndex && key.adaptationSetIndex == adaptationSetIndex); copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, - copyRepresentations, adaptationSet.accessibilityDescriptors)); + copyRepresentations, adaptationSet.accessibilityDescriptors, + adaptationSet.supplementalProperties)); } while(key.periodIndex == periodIndex); // Add back the last key which doesn't belong to the period being processed keys.addFirst(key); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index d4338fd812..53115a7a0e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -239,8 +239,9 @@ public class DashManifestParser extends DefaultHandler int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); ArrayList drmSchemeDatas = new ArrayList<>(); - ArrayList inbandEventStreams = new ArrayList<>(); - ArrayList accessibilityDescriptors = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList accessibilityDescriptors = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); List representationInfos = new ArrayList<>(); @C.SelectionFlags int selectionFlags = 0; @@ -265,7 +266,9 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { - accessibilityDescriptors.add(parseAccessibility(xpp)); + accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs, width, height, frameRate, audioChannels, audioSamplingRate, language, @@ -280,7 +283,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { - inbandEventStreams.add(parseInbandEventStream(xpp)); + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } @@ -293,12 +296,15 @@ public class DashManifestParser extends DefaultHandler drmSchemeDatas, inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors); + return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, + supplementalProperties); } protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations, List accessibilityDescriptors) { - return new AdaptationSet(id, contentType, representations, accessibilityDescriptors); + List representations, List accessibilityDescriptors, + List supplementalProperties) { + return new AdaptationSet(id, contentType, representations, accessibilityDescriptors, + supplementalProperties); } protected int parseContentType(XmlPullParser xpp) { @@ -337,6 +343,7 @@ public class DashManifestParser extends DefaultHandler IOException { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); + String schemeType = xpp.getAttributeValue(null, "value"); byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; @@ -362,34 +369,8 @@ public class DashManifestParser extends DefaultHandler requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) - : null; - } - - /** - * Parses an InbandEventStream element. - * - * @param xpp The parser from which to read. - * @throws XmlPullParserException If an error occurs parsing the element. - * @throws IOException If an error occurs reading the element. - * @return A {@link SchemeValuePair} parsed from the element. - */ - protected SchemeValuePair parseInbandEventStream(XmlPullParser xpp) - throws XmlPullParserException, IOException { - return parseSchemeValuePair(xpp, "InbandEventStream"); - } - - /** - * Parses an Accessibility element. - * - * @param xpp The parser from which to read. - * @throws XmlPullParserException If an error occurs parsing the element. - * @throws IOException If an error occurs reading the element. - * @return A {@link SchemeValuePair} parsed from the element. - */ - protected SchemeValuePair parseAccessibility(XmlPullParser xpp) - throws XmlPullParserException, IOException { - return parseSchemeValuePair(xpp, "Accessibility"); + return data != null + ? new SchemeData(uuid, schemeType, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; } /** @@ -429,7 +410,7 @@ public class DashManifestParser extends DefaultHandler int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, String adaptationSetLanguage, @C.SelectionFlags int adaptationSetSelectionFlags, - List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase) + List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -442,7 +423,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = adaptationSetAudioChannels; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); ArrayList drmSchemeDatas = new ArrayList<>(); - ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -466,7 +447,7 @@ public class DashManifestParser extends DefaultHandler drmSchemeDatas.add(contentProtection); } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { - inbandEventStreams.add(parseInbandEventStream(xpp)); + inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); @@ -480,7 +461,7 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, - @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, + @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, String codecs) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { @@ -509,14 +490,14 @@ public class DashManifestParser extends DefaultHandler protected Representation buildRepresentation(RepresentationInfo representationInfo, String contentId, ArrayList extraDrmSchemeDatas, - ArrayList extraInbandEventStreams) { + ArrayList extraInbandEventStreams) { Format format = representationInfo.format; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas)); } - ArrayList inbandEventStremas = representationInfo.inbandEventStreams; + ArrayList inbandEventStremas = representationInfo.inbandEventStreams; inbandEventStremas.addAll(extraInbandEventStreams); return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas); @@ -809,28 +790,29 @@ public class DashManifestParser extends DefaultHandler } /** - * Parses a {@link SchemeValuePair} from an element. + * Parses a {@link Descriptor} from an element. * * @param xpp The parser from which to read. * @param tag The tag of the element being parsed. * @throws XmlPullParserException If an error occurs parsing the element. * @throws IOException If an error occurs reading the element. - * @return The parsed {@link SchemeValuePair}. + * @return The parsed {@link Descriptor}. */ - protected static SchemeValuePair parseSchemeValuePair(XmlPullParser xpp, String tag) + protected static Descriptor parseDescriptor(XmlPullParser xpp, String tag) throws XmlPullParserException, IOException { - String schemeIdUri = parseString(xpp, "schemeIdUri", null); + String schemeIdUri = parseString(xpp, "schemeIdUri", ""); String value = parseString(xpp, "value", null); + String id = parseString(xpp, "id", null); do { xpp.next(); } while (!XmlPullParserUtil.isEndTag(xpp, tag)); - return new SchemeValuePair(schemeIdUri, value); + return new Descriptor(schemeIdUri, value, id); } protected static int parseCea608AccessibilityChannel( - List accessibilityDescriptors) { + List accessibilityDescriptors) { for (int i = 0; i < accessibilityDescriptors.size(); i++) { - SchemeValuePair descriptor = accessibilityDescriptors.get(i); + Descriptor descriptor = accessibilityDescriptors.get(i); if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri) && descriptor.value != null) { Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value); @@ -845,9 +827,9 @@ public class DashManifestParser extends DefaultHandler } protected static int parseCea708AccessibilityChannel( - List accessibilityDescriptors) { + List accessibilityDescriptors) { for (int i = 0; i < accessibilityDescriptors.size(); i++) { - SchemeValuePair descriptor = accessibilityDescriptors.get(i); + Descriptor descriptor = accessibilityDescriptors.get(i); if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri) && descriptor.value != null) { Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value); @@ -925,10 +907,10 @@ public class DashManifestParser extends DefaultHandler public final String baseUrl; public final SegmentBase segmentBase; public final ArrayList drmSchemeDatas; - public final ArrayList inbandEventStreams; + public final ArrayList inbandEventStreams; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { + ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java similarity index 52% rename from library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java index 470bf0f989..18d0a937ab 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java @@ -15,19 +15,37 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Util; /** - * A pair consisting of a scheme ID and value. + * A descriptor, as defined by ISO 23009-1, 2nd edition, 5.8.2. */ -public class SchemeValuePair { +public final class Descriptor { - public final String schemeIdUri; - public final String value; + /** + * The scheme URI. + */ + @NonNull public final String schemeIdUri; + /** + * The value, or null. + */ + @Nullable public final String value; + /** + * The identifier, or null. + */ + @Nullable public final String id; - public SchemeValuePair(String schemeIdUri, String value) { + /** + * @param schemeIdUri The scheme URI. + * @param value The value, or null. + * @param id The identifier, or null. + */ + public Descriptor(@NonNull String schemeIdUri, @Nullable String value, @Nullable String id) { this.schemeIdUri = schemeIdUri; this.value = value; + this.id = id; } @Override @@ -38,14 +56,17 @@ public class SchemeValuePair { if (obj == null || getClass() != obj.getClass()) { return false; } - SchemeValuePair other = (SchemeValuePair) obj; - return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value); + Descriptor other = (Descriptor) obj; + return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value) + && Util.areEqual(id, other.id); } @Override public int hashCode() { - return 31 * (schemeIdUri != null ? schemeIdUri.hashCode() : 0) - + (value != null ? value.hashCode() : 0); + int result = (schemeIdUri != null ? schemeIdUri.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (id != null ? id.hashCode() : 0); + return result; } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 5960d4d7ba..81e4602c1d 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -65,7 +65,7 @@ public abstract class Representation { /** * The in-band event streams in the representation. Never null, but may be empty. */ - public final List inbandEventStreams; + public final List inbandEventStreams; private final RangedUri initializationUri; @@ -96,7 +96,7 @@ public abstract class Representation { * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { + String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams, null); } @@ -115,7 +115,7 @@ public abstract class Representation { * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - String baseUrl, SegmentBase segmentBase, List inbandEventStreams, + String baseUrl, SegmentBase segmentBase, List inbandEventStreams, String customCacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl, @@ -130,13 +130,12 @@ public abstract class Representation { } private Representation(String contentId, long revisionId, Format format, String baseUrl, - SegmentBase segmentBase, List inbandEventStreams) { + SegmentBase segmentBase, List inbandEventStreams) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; - this.inbandEventStreams = inbandEventStreams == null - ? Collections.emptyList() + this.inbandEventStreams = inbandEventStreams == null ? Collections.emptyList() : Collections.unmodifiableList(inbandEventStreams); initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); @@ -201,8 +200,8 @@ public abstract class Representation { */ public static SingleSegmentRepresentation newInstance(String contentId, long revisionId, Format format, String uri, long initializationStart, long initializationEnd, - long indexStart, long indexEnd, List inbandEventStreams, - String customCacheKey, long contentLength) { + long indexStart, long indexEnd, List inbandEventStreams, String customCacheKey, + long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart, @@ -222,7 +221,7 @@ public abstract class Representation { * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ public SingleSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, + String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, String customCacheKey, long contentLength) { super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); @@ -270,7 +269,7 @@ public abstract class Representation { * @param inbandEventStreams The in-band event streams in the representation. May be null. */ public MultiSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { + String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 47b5758b1d..ac77725ca5 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -23,14 +24,16 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion diff --git a/library/hls/src/androidTest/AndroidManifest.xml b/library/hls/src/androidTest/AndroidManifest.xml index ac0857fc3f..dcf6c2f940 100644 --- a/library/hls/src/androidTest/AndroidManifest.xml +++ b/library/hls/src/androidTest/AndroidManifest.xml @@ -28,7 +28,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 795e2f0eaa..bca62ed230 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -39,7 +39,6 @@ import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; import java.util.List; -import java.util.Locale; /** * Source of Hls (possibly adaptive) chunks. @@ -165,6 +164,13 @@ import java.util.Locale; this.trackSelection = trackSelection; } + /** + * Returns the current track selection. + */ + public TrackSelection getTrackSelection() { + return trackSelection; + } + /** * Resets the source. */ @@ -222,8 +228,9 @@ import java.util.Locale; // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { - long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs; - if (!mediaPlaylist.hasEndTag && targetPositionUs > mediaPlaylist.getEndTimeUs()) { + long targetPositionUs = previous == null ? playbackPositionUs + : mediaPlaylist.hasIndependentSegmentsTag ? previous.endTimeUs : previous.startTimeUs; + if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { @@ -358,7 +365,7 @@ import java.util.Locale; private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) { String trimmedIv; - if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { + if (Util.toLowerInvariant(iv).startsWith("0x")) { trimmedIv = iv.substring(2); } else { trimmedIv = iv; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 6997324f02..29b7e4a6a8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -210,7 +210,7 @@ import java.util.concurrent.atomic.AtomicInteger; // According to spec, for packed audio, initDataSpec is expected to be null. return; } - DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); + DataSpec initSegmentDataSpec = initDataSpec.subrange(initSegmentBytesLoaded); try { ExtractorInput input = new DefaultExtractorInput(initDataSource, initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec)); @@ -239,7 +239,7 @@ import java.util.concurrent.atomic.AtomicInteger; loadDataSpec = dataSpec; skipLoadedBytes = bytesLoaded != 0; } else { - loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + loadDataSpec = dataSpec.subrange(bytesLoaded); skipLoadedBytes = false; } if (!isMasterTimestampSource) { @@ -396,7 +396,7 @@ import java.util.concurrent.atomic.AtomicInteger; } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { extractor = new Mp3Extractor(0, startTimeUs); } else { - throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); + throw new IllegalArgumentException("Unknown extension for audio file: " + lastPathSegment); } extractor.init(extractorOutput); return extractor; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 3a833f5468..003b38efef 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; @@ -51,19 +52,16 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; - private final long preparePositionUs; private Callback callback; private int pendingPrepareCount; - private boolean seenFirstTrackSelection; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator, - long positionUs) { + int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator) { this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; @@ -72,32 +70,29 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); - preparePositionUs = positionUs; + sampleStreamWrappers = new HlsSampleStreamWrapper[0]; + enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; } public void release() { playlistTracker.removeListener(this); continueLoadingHandler.removeCallbacksAndMessages(null); - if (sampleStreamWrappers != null) { - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - sampleStreamWrapper.release(); - } + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.release(); } } @Override - public void prepare(Callback callback) { - playlistTracker.addListener(this); + public void prepare(Callback callback, long positionUs) { this.callback = callback; - buildAndPrepareSampleStreamWrappers(); + playlistTracker.addListener(this); + buildAndPrepareSampleStreamWrappers(positionUs); } @Override public void maybeThrowPrepareError() throws IOException { - if (sampleStreamWrappers != null) { - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - sampleStreamWrapper.maybeThrowPrepareError(); - } + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.maybeThrowPrepareError(); } } @@ -126,21 +121,24 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } } - boolean selectedNewTracks = false; + + boolean forceReset = false; streamWrapperIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. SampleStream[] newStreams = new SampleStream[selections.length]; SampleStream[] childStreams = new SampleStream[selections.length]; TrackSelection[] childSelections = new TrackSelection[selections.length]; - ArrayList enabledSampleStreamWrapperList = new ArrayList<>( - sampleStreamWrappers.length); + int newEnabledSampleStreamWrapperCount = 0; + HlsSampleStreamWrapper[] newEnabledSampleStreamWrappers = + new HlsSampleStreamWrapper[sampleStreamWrappers.length]; for (int i = 0; i < sampleStreamWrappers.length; i++) { for (int j = 0; j < selections.length; j++) { childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; } - selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections, - mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection); + HlsSampleStreamWrapper sampleStreamWrapper = sampleStreamWrappers[i]; + boolean wasReset = sampleStreamWrapper.selectTracks(childSelections, mayRetainStreamFlags, + childStreams, streamResetFlags, positionUs, forceReset); boolean wrapperEnabled = false; for (int j = 0; j < selections.length; j++) { if (selectionChildIndices[j] == i) { @@ -155,43 +153,37 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } if (wrapperEnabled) { - enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]); + newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper; + if (newEnabledSampleStreamWrapperCount++ == 0) { + // The first enabled wrapper is responsible for initializing timestamp adjusters. This + // way, if enabled, variants are responsible. Else audio renditions. Else text renditions. + sampleStreamWrapper.setIsTimestampMaster(true); + if (wasReset || enabledSampleStreamWrappers.length == 0 + || sampleStreamWrapper != enabledSampleStreamWrappers[0]) { + // The wrapper responsible for initializing the timestamp adjusters was reset or + // changed. We need to reset the timestamp adjuster provider and all other wrappers. + timestampAdjusterProvider.reset(); + forceReset = true; + } + } else { + sampleStreamWrapper.setIsTimestampMaster(false); + } } } // Copy the new streams back into the streams array. System.arraycopy(newStreams, 0, streams, 0, newStreams.length); // Update the local state. - enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()]; - enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers); - - // The first enabled sample stream wrapper is responsible for intializing the timestamp - // adjuster. This way, if present, variants are responsible. Otherwise, audio renditions are. - // If only subtitles are present, then text renditions are used for timestamp adjustment - // initialization. - if (enabledSampleStreamWrappers.length > 0) { - enabledSampleStreamWrappers[0].setIsTimestampMaster(true); - for (int i = 1; i < enabledSampleStreamWrappers.length; i++) { - enabledSampleStreamWrappers[i].setIsTimestampMaster(false); - } - } - + enabledSampleStreamWrappers = Arrays.copyOf(newEnabledSampleStreamWrappers, + newEnabledSampleStreamWrapperCount); sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); - if (seenFirstTrackSelection && selectedNewTracks) { - seekToUs(positionUs); - // We'll need to reset renderers consuming from all streams due to the seek. - for (int i = 0; i < selections.length; i++) { - if (streams[i] != null) { - streamResetFlags[i] = true; - } - } - } - seenFirstTrackSelection = true; return positionUs; } @Override public void discardBuffer(long positionUs) { - // Do nothing. + for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { + sampleStreamWrapper.discardBuffer(positionUs); + } } @Override @@ -211,21 +203,21 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public long getBufferedPositionUs() { - long bufferedPositionUs = Long.MAX_VALUE; - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - long rendererBufferedPositionUs = sampleStreamWrapper.getBufferedPositionUs(); - if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } - return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + return sequenceableLoader.getBufferedPositionUs(); } @Override public long seekToUs(long positionUs) { - timestampAdjusterProvider.reset(); - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - sampleStreamWrapper.seekTo(positionUs); + if (enabledSampleStreamWrappers.length > 0) { + // We need to reset all wrappers if the one responsible for initializing timestamp adjusters + // is reset. Else each wrapper can decide whether to reset independently. + boolean forceReset = enabledSampleStreamWrappers[0].seekToUs(positionUs, false); + for (int i = 1; i < enabledSampleStreamWrappers.length; i++) { + enabledSampleStreamWrappers[i].seekToUs(positionUs, forceReset); + } + if (forceReset) { + timestampAdjusterProvider.reset(); + } } return positionUs; } @@ -285,7 +277,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Internal methods. - private void buildAndPrepareSampleStreamWrappers() { + private void buildAndPrepareSampleStreamWrappers(long positionUs) { HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); // Build the default stream wrapper. List selectedVariants = new ArrayList<>(masterPlaylist.variants); @@ -322,7 +314,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats); + variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); @@ -332,7 +324,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Build audio stream wrappers. for (int i = 0; i < audioRenditions.size(); i++) { sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList()); + new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList(), positionUs); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.continuePreparing(); } @@ -341,18 +333,21 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (int i = 0; i < subtitleRenditions.size(); i++) { HlsUrl url = subtitleRenditions.get(i); sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - Collections.emptyList()); + Collections.emptyList(), positionUs); sampleStreamWrapper.prepareSingleTrack(url.format); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; } + + // All wrappers are enabled during preparation. + enabledSampleStreamWrappers = sampleStreamWrappers; } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, - Format muxedAudioFormat, List muxedCaptionFormats) { + Format muxedAudioFormat, List muxedCaptionFormats, long positionUs) { HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); - return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); + return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, positionUs, + muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } private void continuePreparingOrLoading() { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 1bfb8371a0..fd3d533337 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; @@ -38,6 +39,10 @@ import java.util.List; public final class HlsMediaSource implements MediaSource, HlsPlaylistTracker.PrimaryPlaylistListener { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -88,10 +93,10 @@ public final class HlsMediaSource implements MediaSource, } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount, - eventDispatcher, allocator, positionUs); + eventDispatcher, allocator); } @Override @@ -111,6 +116,9 @@ public final class HlsMediaSource implements MediaSource, @Override public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { SinglePeriodTimeline timeline; + long presentationStartTimeMs = playlist.hasProgramDateTime ? 0 : C.TIME_UNSET; + long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) + : C.TIME_UNSET; long windowDefaultStartPositionUs = playlist.startOffsetUs; if (playlistTracker.isLive()) { long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs) @@ -120,14 +128,16 @@ public final class HlsMediaSource implements MediaSource, windowDefaultStartPositionUs = segments.isEmpty() ? 0 : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; } - timeline = new SinglePeriodTimeline(periodDurationUs, playlist.durationUs, - playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs, + periodDurationUs, playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, + true, !playlist.hasEndTag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { windowDefaultStartPositionUs = 0; } - timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, - playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false); + timeline = new SinglePeriodTimeline(presentationStartTimeMs, windowStartTimeMs, + playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs, + windowDefaultStartPositionUs, true, false); } sourceListener.onSourceInfoRefreshed(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 827a6e885d..0b6d1863bd 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -17,16 +17,15 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; import android.text.TextUtils; -import android.util.SparseArray; 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.extractor.DefaultTrackOutput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; @@ -39,7 +38,9 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.Arrays; import java.util.LinkedList; /** @@ -47,7 +48,7 @@ import java.util.LinkedList; * {@link SampleStream}s from which the loaded media can be consumed. */ /* package */ final class HlsSampleStreamWrapper implements Loader.Callback, - SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { + Loader.ReleaseCallback, SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener { /** * A callback to be notified of events. @@ -81,11 +82,12 @@ import java.util.LinkedList; private final Loader loader; private final EventDispatcher eventDispatcher; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; - private final SparseArray sampleQueues; private final LinkedList mediaChunks; private final Runnable maybeFinishPrepareRunnable; private final Handler handler; + private SampleQueue[] sampleQueues; + private int[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; private boolean prepared; private int enabledTrackCount; @@ -97,12 +99,15 @@ import java.util.LinkedList; // Indexed by track (as exposed by this source). private TrackGroupArray trackGroups; private int primaryTrackGroupIndex; - // Indexed by group. - private boolean[] groupEnabledStates; + private boolean haveAudioVideoTrackGroups; + // Indexed by track group. + private boolean[] trackGroupEnabledStates; + private boolean[] trackGroupIsAudioVideoFlags; private long lastSeekPositionUs; private long pendingResetPositionUs; - + private boolean pendingResetUpstreamFormats; + private boolean seenFirstTrackSelection; private boolean loadingFinished; /** @@ -128,7 +133,8 @@ import java.util.LinkedList; this.eventDispatcher = eventDispatcher; loader = new Loader("Loader:HlsSampleStreamWrapper"); nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); - sampleQueues = new SparseArray<>(); + sampleQueueTrackIds = new int[0]; + sampleQueues = new SampleQueue[0]; mediaChunks = new LinkedList<>(); maybeFinishPrepareRunnable = new Runnable() { @Override @@ -165,78 +171,153 @@ import java.util.LinkedList; return trackGroups; } + /** + * Called by the parent {@link HlsMediaPeriod} when a track selection occurs. + * + * @param selections The renderer track selections. + * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained + * for each selection. A {@code true} value indicates that the selection is unchanged, and + * that the caller does not require that the sample stream be recreated. + * @param streams The existing sample streams, which will be updated to reflect the provided + * selections. + * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that + * have been retained but with the requirement that the consuming renderer be reset. + * @param positionUs The current playback position in microseconds. + * @param forceReset If true then a reset is forced (i.e. a seek will be performed with in-buffer + * seeking disabled). + * @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as + * part of the track selection. + */ public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) { + SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) { Assertions.checkState(prepared); - // Disable old tracks. + int oldEnabledTrackCount = enabledTrackCount; + // Deselect old tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { int group = ((HlsSampleStream) streams[i]).group; setTrackGroupEnabledState(group, false); - sampleQueues.valueAt(group).disable(); streams[i] = null; } } - // Enable new tracks. - TrackSelection primaryTrackSelection = null; - boolean selectedNewTracks = false; + // We'll always need to seek if we're being forced to reset, or if this is a first selection to + // a position other than the one we started preparing with, or if we're making a selection + // having previously disabled all tracks. + boolean seekRequired = forceReset + || (seenFirstTrackSelection ? oldEnabledTrackCount == 0 : positionUs != lastSeekPositionUs); + // Get the old (i.e. current before the loop below executes) primary track selection. The new + // primary selection will equal the old one unless it's changed in the loop. + TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection(); + TrackSelection primaryTrackSelection = oldPrimaryTrackSelection; + // Select new tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; - int group = trackGroups.indexOf(selection.getTrackGroup()); - setTrackGroupEnabledState(group, true); - if (group == primaryTrackGroupIndex) { + int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); + setTrackGroupEnabledState(trackGroupIndex, true); + if (trackGroupIndex == primaryTrackGroupIndex) { primaryTrackSelection = selection; chunkSource.selectTracks(selection); } - streams[i] = new HlsSampleStream(this, group); + streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; - selectedNewTracks = true; - } - } - if (isFirstTrackSelection) { - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - if (!groupEnabledStates[i]) { - sampleQueues.valueAt(i).disable(); - } - } - if (primaryTrackSelection != null && !mediaChunks.isEmpty()) { - primaryTrackSelection.updateSelectedTrack(0); - int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); - if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { - // The loaded preparation chunk does match the selection. We discard it. - seekTo(lastSeekPositionUs); + // If there's still a chance of avoiding a seek, try and seek within the sample queue. + if (!seekRequired) { + SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; + sampleQueue.rewind(); + // A seek can be avoided if we're able to advance to the current playback position in the + // sample queue, or if we haven't read anything from the queue since the previous seek + // (this case is common for sparse tracks such as metadata tracks). In all other cases a + // seek is required. + seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + && sampleQueue.getReadIndex() != 0; } } } - // Cancel requests if necessary. + if (enabledTrackCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; mediaChunks.clear(); if (loader.isLoading()) { + // Discard as much as we can synchronously. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } loader.cancelLoading(); + } else { + resetSampleQueues(); + } + } else { + if (!mediaChunks.isEmpty() + && !Util.areEqual(primaryTrackSelection, oldPrimaryTrackSelection)) { + // The primary track selection has changed and we have buffered media. The buffered media + // may need to be discarded. + boolean primarySampleQueueDirty = false; + if (!seenFirstTrackSelection) { + primaryTrackSelection.updateSelectedTrack(0); + int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); + if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { + // This is the first selection and the chunk loaded during preparation does not match + // the initially selected format. + primarySampleQueueDirty = true; + } + } else { + // The primary sample queue contains media buffered for the old primary track selection. + primarySampleQueueDirty = true; + } + if (primarySampleQueueDirty) { + forceReset = true; + seekRequired = true; + pendingResetUpstreamFormats = true; + } + } + if (seekRequired) { + seekToUs(positionUs, forceReset); + // We'll need to reset renderers consuming from all streams due to the seek. + for (int i = 0; i < streams.length; i++) { + if (streams[i] != null) { + streamResetFlags[i] = true; + } + } } } - return selectedNewTracks; + + seenFirstTrackSelection = true; + return seekRequired; } - public void seekTo(long positionUs) { + public void discardBuffer(long positionUs) { + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { + sampleQueues[i].discardTo(positionUs, false, trackGroupEnabledStates[i]); + } + } + + /** + * Attempts to seek to the specified position in microseconds. + * + * @param positionUs The seek position in microseconds. + * @param forceReset If true then a reset is forced (i.e. in-buffer seeking is disabled). + * @return Whether the wrapper was reset, meaning the wrapped sample queues were reset. If false, + * an in-buffer seek was performed. + */ + public boolean seekToUs(long positionUs, boolean forceReset) { lastSeekPositionUs = positionUs; + // If we're not forced to reset nor have a pending reset, see if we can seek within the buffer. + if (!forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) { + return false; + } + // We were unable to seek within the buffer, so need to reset. pendingResetPositionUs = positionUs; loadingFinished = false; mediaChunks.clear(); if (loader.isLoading()) { loader.cancelLoading(); } else { - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).reset(groupEnabledStates[i]); - } + resetSampleQueues(); } + return true; } public long getBufferedPositionUs() { @@ -252,25 +333,32 @@ import java.util.LinkedList; if (lastCompletedMediaChunk != null) { bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); } - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { + for (SampleQueue sampleQueue : sampleQueues) { bufferedPositionUs = Math.max(bufferedPositionUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); + sampleQueue.getLargestQueuedTimestampUs()); } return bufferedPositionUs; } } public void release() { - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).disable(); + boolean releasedSynchronously = loader.release(this); + if (prepared && !releasedSynchronously) { + // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise + // sampleQueues may still be being modified by the loading thread. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } } - loader.release(); handler.removeCallbacksAndMessages(null); released = true; } + @Override + public void onLoaderReleased() { + resetSampleQueues(); + } + public void setIsTimestampMaster(boolean isTimestampMaster) { chunkSource.setIsTimestampMaster(isTimestampMaster); } @@ -281,8 +369,8 @@ import java.util.LinkedList; // SampleStream implementation. - /* package */ boolean isReady(int group) { - return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(group).isEmpty()); + /* package */ boolean isReady(int trackGroupIndex) { + return loadingFinished || (!isPendingReset() && sampleQueues[trackGroupIndex].hasNextSample()); } /* package */ void maybeThrowError() throws IOException { @@ -290,47 +378,56 @@ import java.util.LinkedList; chunkSource.maybeThrowError(); } - /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean requireFormat) { + /* package */ int readData(int trackGroupIndex, FormatHolder formatHolder, + DecoderInputBuffer buffer, boolean requireFormat) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { - mediaChunks.removeFirst(); + if (!mediaChunks.isEmpty()) { + while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { + mediaChunks.removeFirst(); + } + HlsMediaChunk currentChunk = mediaChunks.getFirst(); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(downstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(trackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + downstreamTrackFormat = trackFormat; } - HlsMediaChunk currentChunk = mediaChunks.getFirst(); - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(downstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(trackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - downstreamTrackFormat = trackFormat; - return sampleQueues.valueAt(group).readData(formatHolder, buffer, requireFormat, - loadingFinished, lastSeekPositionUs); + return sampleQueues[trackGroupIndex].read(formatHolder, buffer, requireFormat, loadingFinished, + lastSeekPositionUs); } - /* package */ void skipData(int group, long positionUs) { - DefaultTrackOutput sampleQueue = sampleQueues.valueAt(group); + /* package */ void skipData(int trackGroupIndex, long positionUs) { + SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.skipAll(); + sampleQueue.advanceToEnd(); } else { - sampleQueue.skipToKeyframeBefore(positionUs, true); + sampleQueue.advanceTo(positionUs, true, true); } } private boolean finishedReadingChunk(HlsMediaChunk chunk) { int chunkUid = chunk.uid; - for (int i = 0; i < sampleQueues.size(); i++) { - if (groupEnabledStates[i] && sampleQueues.valueAt(i).peekSourceId() == chunkUid) { + for (int i = 0; i < sampleQueues.length; i++) { + if (trackGroupEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { return false; } } return true; } + private void resetSampleQueues() { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(pendingResetUpstreamFormats); + } + pendingResetUpstreamFormats = false; + } + // SequenceableLoader implementation @Override @@ -348,6 +445,7 @@ import java.util.LinkedList; nextChunkHolder.clear(); if (endOfStream) { + pendingResetPositionUs = C.TIME_UNSET; loadingFinished = true; return true; } @@ -403,11 +501,10 @@ import java.util.LinkedList; loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs, loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); if (!released) { - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - sampleQueues.valueAt(i).reset(groupEnabledStates[i]); + resetSampleQueues(); + if (enabledTrackCount > 0) { + callback.onContinueLoadingRequested(this); } - callback.onContinueLoadingRequested(this); } } @@ -455,12 +552,12 @@ import java.util.LinkedList; */ public void init(int chunkUid, boolean shouldSpliceIn) { upstreamChunkUid = chunkUid; - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).sourceId(chunkUid); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.sourceId(chunkUid); } if (shouldSpliceIn) { - for (int i = 0; i < sampleQueues.size(); i++) { - sampleQueues.valueAt(i).splice(); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.splice(); } } } @@ -468,14 +565,19 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id, int type) { - if (sampleQueues.indexOfKey(id) >= 0) { - return sampleQueues.get(id); + public SampleQueue track(int id, int type) { + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + if (sampleQueueTrackIds[i] == id) { + return sampleQueues[i]; + } } - DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); + SampleQueue trackOutput = new SampleQueue(allocator); trackOutput.setUpstreamFormatChangeListener(this); - trackOutput.sourceId(upstreamChunkUid); - sampleQueues.put(id, trackOutput); + sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); + sampleQueueTrackIds[trackCount] = id; + sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); + sampleQueues[trackCount] = trackOutput; return trackOutput; } @@ -503,9 +605,8 @@ import java.util.LinkedList; if (released || prepared || !sampleQueuesBuilt) { return; } - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { + for (SampleQueue sampleQueue : sampleQueues) { + if (sampleQueue.getUpstreamFormat() == null) { return; } } @@ -548,9 +649,9 @@ import java.util.LinkedList; // of the single track of this type. int primaryExtractorTrackType = PRIMARY_TYPE_NONE; int primaryExtractorTrackIndex = C.INDEX_UNSET; - int extractorTrackCount = sampleQueues.size(); + int extractorTrackCount = sampleQueues.length; for (int i = 0; i < extractorTrackCount; i++) { - String sampleMimeType = sampleQueues.valueAt(i).getUpstreamFormat().sampleMimeType; + String sampleMimeType = sampleQueues[i].getUpstreamFormat().sampleMimeType; int trackType; if (MimeTypes.isVideo(sampleMimeType)) { trackType = PRIMARY_TYPE_VIDEO; @@ -577,12 +678,17 @@ import java.util.LinkedList; // Instantiate the necessary internal data-structures. primaryTrackGroupIndex = C.INDEX_UNSET; - groupEnabledStates = new boolean[extractorTrackCount]; + trackGroupEnabledStates = new boolean[extractorTrackCount]; + trackGroupIsAudioVideoFlags = new boolean[extractorTrackCount]; // Construct the set of exposed track groups. TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount]; for (int i = 0; i < extractorTrackCount; i++) { - Format sampleFormat = sampleQueues.valueAt(i).getUpstreamFormat(); + Format sampleFormat = sampleQueues[i].getUpstreamFormat(); + String mimeType = sampleFormat.sampleMimeType; + boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType); + trackGroupIsAudioVideoFlags[i] = isAudioVideo; + haveAudioVideoTrackGroups |= isAudioVideo; if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; for (int j = 0; j < chunkSourceTrackCount; j++) { @@ -602,12 +708,12 @@ import java.util.LinkedList; /** * Enables or disables a specified track group. * - * @param group The index of the track group. + * @param trackGroupIndex The index of the track group. * @param enabledState True if the group is being enabled, or false if it's being disabled. */ - private void setTrackGroupEnabledState(int group, boolean enabledState) { - Assertions.checkState(groupEnabledStates[group] != enabledState); - groupEnabledStates[group] = enabledState; + private void setTrackGroupEnabledState(int trackGroupIndex, boolean enabledState) { + Assertions.checkState(trackGroupEnabledStates[trackGroupIndex] != enabledState); + trackGroupEnabledStates[trackGroupIndex] = enabledState; enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1); } @@ -643,6 +749,30 @@ import java.util.LinkedList; return pendingResetPositionUs != C.TIME_UNSET; } + /** + * Attempts to seek to the specified position within the sample queues. + * + * @param positionUs The seek position in microseconds. + * @return Whether the in-buffer seek was successful. + */ + private boolean seekInsideBufferUs(long positionUs) { + int trackCount = sampleQueues.length; + for (int i = 0; i < trackCount; i++) { + SampleQueue sampleQueue = sampleQueues[i]; + sampleQueue.rewind(); + boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false); + // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue + // is successful. We ignore whether seeks within non-AV queues are successful in this case, as + // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is + // successful only if the seek into every queue succeeds. + if (!seekInsideQueue && (trackGroupIsAudioVideoFlags[i] || !haveAudioVideoTrackGroups)) { + return false; + } + sampleQueue.discardToRead(); + } + return true; + } + private static String getAudioCodecs(String codecs) { return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 874c865049..b38763f7e8 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -88,16 +88,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public final List muxedCaptionFormats; /** - * @param baseUri The base uri. Used to resolve relative paths. + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. * @param variants See {@link #variants}. * @param audios See {@link #audios}. * @param subtitles See {@link #subtitles}. * @param muxedAudioFormat See {@link #muxedAudioFormat}. * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. */ - public HlsMasterPlaylist(String baseUri, List variants, List audios, - List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { - super(baseUri); + public HlsMasterPlaylist(String baseUri, List tags, List variants, + List audios, List subtitles, Format muxedAudioFormat, + List muxedCaptionFormats) { + super(baseUri, tags); this.variants = Collections.unmodifiableList(variants); this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); @@ -115,7 +117,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); + return new HlsMasterPlaylist(null, Collections.emptyList(), variant, emptyList, + emptyList, null, null); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 69b95e6d3d..db4f041be2 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -33,24 +33,64 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public static final class Segment implements Comparable { + /** + * The url of the segment. + */ public final String url; + /** + * The duration of the segment in microseconds, as defined by #EXTINF. + */ public final long durationUs; + /** + * The number of #EXT-X-DISCONTINUITY tags in the playlist before the segment. + */ public final int relativeDiscontinuitySequence; + /** + * The start time of the segment in microseconds, relative to the start of the playlist. + */ public final long relativeStartTimeUs; + /** + * Whether the segment is encrypted, as defined by #EXT-X-KEY. + */ public final boolean isEncrypted; + /** + * The encryption key uri as defined by #EXT-X-KEY, or null if the segment is not encrypted. + */ public final String encryptionKeyUri; + /** + * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not + * encrypted. + */ public final String encryptionIV; + /** + * The segment's byte range offset, as defined by #EXT-X-BYTERANGE. + */ public final long byterangeOffset; + /** + * The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if + * no byte range is specified. + */ public final long byterangeLength; public Segment(String uri, long byterangeOffset, long byterangeLength) { this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength); } - public Segment(String uri, long durationUs, int relativeDiscontinuitySequence, + /** + * @param url See {@link #url}. + * @param durationUs See {@link #durationUs}. + * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. + * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. + * @param isEncrypted See {@link #isEncrypted}. + * @param encryptionKeyUri See {@link #encryptionKeyUri}. + * @param encryptionIV See {@link #encryptionIV}. + * @param byterangeOffset See {@link #byterangeOffset}. + * @param byterangeLength See {@link #byterangeLength}. + */ + public Segment(String url, long durationUs, int relativeDiscontinuitySequence, long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { - this.url = uri; + this.url = url; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; @@ -70,7 +110,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } /** - * Type of the playlist as specified by #EXT-X-PLAYLIST-TYPE. + * Type of the playlist as defined by #EXT-X-PLAYLIST-TYPE. */ @Retention(RetentionPolicy.SOURCE) @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT}) @@ -79,27 +119,88 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public static final int PLAYLIST_TYPE_VOD = 1; public static final int PLAYLIST_TYPE_EVENT = 2; + /** + * The type of the playlist. See {@link PlaylistType}. + */ @PlaylistType public final int playlistType; + /** + * The start offset in microseconds, as defined by #EXT-X-START. + */ public final long startOffsetUs; + /** + * The start time of the playlist in playback timebase in microseconds. + */ public final long startTimeUs; + /** + * Whether the playlist contains the #EXT-X-DISCONTINUITY-SEQUENCE tag. + */ public final boolean hasDiscontinuitySequence; + /** + * The discontinuity sequence number of the first media segment in the playlist, as defined by + * #EXT-X-DISCONTINUITY-SEQUENCE. + */ public final int discontinuitySequence; + /** + * The media sequence number of the first media segment in the playlist, as defined by + * #EXT-X-MEDIA-SEQUENCE. + */ public final int mediaSequence; + /** + * The compatibility version, as defined by #EXT-X-VERSION. + */ public final int version; + /** + * The target duration in microseconds, as defined by #EXT-X-TARGETDURATION. + */ public final long targetDurationUs; + /** + * Whether the playlist contains the #EXT-X-INDEPENDENT-SEGMENTS tag. + */ + public final boolean hasIndependentSegmentsTag; + /** + * Whether the playlist contains the #EXT-X-ENDLIST tag. + */ public final boolean hasEndTag; + /** + * Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag. + */ public final boolean hasProgramDateTime; + /** + * The initialization segment, as defined by #EXT-X-MAP. + */ public final Segment initializationSegment; + /** + * The list of segments in the playlist. + */ public final List segments; - public final List dateRanges; + /** + * The total duration of the playlist in microseconds. + */ public final long durationUs; - public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, - long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, - int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, - boolean hasProgramDateTime, Segment initializationSegment, List segments, - List dateRanges) { - super(baseUri); + /** + * @param playlistType See {@link #playlistType}. + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + * @param startOffsetUs See {@link #startOffsetUs}. + * @param startTimeUs See {@link #startTimeUs}. + * @param hasDiscontinuitySequence See {@link #hasDiscontinuitySequence}. + * @param discontinuitySequence See {@link #discontinuitySequence}. + * @param mediaSequence See {@link #mediaSequence}. + * @param version See {@link #version}. + * @param targetDurationUs See {@link #targetDurationUs}. + * @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}. + * @param hasEndTag See {@link #hasEndTag}. + * @param hasProgramDateTime See {@link #hasProgramDateTime}. + * @param initializationSegment See {@link #initializationSegment}. + * @param segments See {@link #segments}. + */ + public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, List tags, + long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence, + int discontinuitySequence, int mediaSequence, int version, long targetDurationUs, + boolean hasIndependentSegmentsTag, boolean hasEndTag, boolean hasProgramDateTime, + Segment initializationSegment, List segments) { + super(baseUri, tags); this.playlistType = playlistType; this.startTimeUs = startTimeUs; this.hasDiscontinuitySequence = hasDiscontinuitySequence; @@ -107,6 +208,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.mediaSequence = mediaSequence; this.version = version; this.targetDurationUs = targetDurationUs; + this.hasIndependentSegmentsTag = hasIndependentSegmentsTag; this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; this.initializationSegment = initializationSegment; @@ -119,7 +221,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; - this.dateRanges = Collections.unmodifiableList(dateRanges); } /** @@ -142,6 +243,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { || (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag); } + /** + * Returns the result of adding the duration of the playlist to its start time. + */ public long getEndTimeUs() { return startTimeUs + durationUs; } @@ -156,9 +260,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @return The playlist. */ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { - return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, - discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, - hasProgramDateTime, initializationSegment, segments, dateRanges); + return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, true, + discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag, + hasEndTag, hasProgramDateTime, initializationSegment, segments); } /** @@ -171,9 +275,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { if (this.hasEndTag) { return this; } - return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, + return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - true, hasProgramDateTime, initializationSegment, segments, dateRanges); + hasIndependentSegmentsTag, true, hasProgramDateTime, initializationSegment, segments); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java index 7c3d64d701..a490c9477c 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java @@ -15,15 +15,30 @@ */ package com.google.android.exoplayer2.source.hls.playlist; +import java.util.Collections; +import java.util.List; + /** * Represents an HLS playlist. */ public abstract class HlsPlaylist { + /** + * The base uri. Used to resolve relative paths. + */ public final String baseUri; + /** + * The list of tags in the playlist. + */ + public final List tags; - protected HlsPlaylist(String baseUri) { + /** + * @param baseUri See {@link #baseUri}. + * @param tags See {@link #tags}. + */ + protected HlsPlaylist(String baseUri, List tags) { this.baseUri = baseUri; + this.tags = Collections.unmodifiableList(tags); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index c64b98b5f3..09d6fcfa18 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -44,6 +44,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); + ArrayList tags = new ArrayList<>(); Format muxedAudioFormat = null; List muxedCaptionFormats = null; boolean noClosedCaptions = false; @@ -186,6 +189,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); - List dateRanges = new ArrayList<>(); + List tags = new ArrayList<>(); long segmentDurationUs = 0; boolean hasDiscontinuitySequence = false; @@ -305,6 +315,12 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser + android:name="android.test.InstrumentationTestRunner"/> diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 43cd4a9f8d..1cc2a6833d 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -68,8 +68,9 @@ import java.util.ArrayList; ProtectionElement protectionElement = manifest.protectionElement; if (protectionElement != null) { byte[] keyId = getProtectionElementKeyId(protectionElement.data); + // We assume pattern encryption does not apply. trackEncryptionBoxes = new TrackEncryptionBox[] { - new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)}; + new TrackEncryptionBox(true, null, INITIALIZATION_VECTOR_SIZE, keyId, 0, 0, null)}; } else { trackEncryptionBoxes = null; } @@ -93,7 +94,7 @@ import java.util.ArrayList; } @Override - public void prepare(Callback callback) { + public void prepare(Callback callback, long positionUs) { this.callback = callback; callback.onPrepared(this); } @@ -158,14 +159,7 @@ import java.util.ArrayList; @Override public long getBufferedPositionUs() { - long bufferedPositionUs = Long.MAX_VALUE; - for (ChunkSampleStream sampleStream : sampleStreams) { - long rendererBufferedPositionUs = sampleStream.getBufferedPositionUs(); - if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } - return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs; + return sequenceableLoader.getBufferedPositionUs(); } @Override diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index d16620d5b2..885d5bd227 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -20,6 +20,7 @@ import android.os.Handler; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; @@ -46,6 +47,10 @@ import java.util.ArrayList; public final class SsMediaSource implements MediaSource, Loader.Callback> { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -222,8 +227,8 @@ public final class SsMediaSource implements MediaSource, } @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - Assertions.checkArgument(index == 0); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkArgument(id.periodIndex == 0); SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, eventDispatcher, manifestLoaderErrorThrower, allocator); mediaPeriods.add(period); diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 3ca5f8d997..5784cc7bc6 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -375,7 +375,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { StreamElement[] streamElementArray = new StreamElement[streamElements.size()]; streamElements.toArray(streamElementArray); if (protectionElement != null) { - DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, + DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, null, MimeTypes.VIDEO_MP4, protectionElement.data)); for (StreamElement streamElement : streamElementArray) { for (int i = 0; i < streamElement.formats.length; i++) { diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 96dcd52655..89734ed806 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../../constants.gradle' apply plugin: 'com.android.library' android { @@ -23,14 +24,16 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 38c7a5be9c..cb5e3465f8 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -15,22 +15,24 @@ */ package com.google.android.exoplayer2.ui; +import android.annotation.SuppressLint; import android.widget.TextView; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.util.Locale; /** * A helper class for periodically updating a {@link TextView} with debug information obtained from * a {@link SimpleExoPlayer}. */ -public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListener { +public final class DebugTextViewHelper implements Runnable, Player.EventListener { private static final int REFRESH_INTERVAL_MS = 1000; @@ -74,7 +76,7 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe textView.removeCallbacks(this); } - // ExoPlayer.EventListener implementation. + // Player.EventListener implementation. @Override public void onLoadingChanged(boolean isLoading) { @@ -86,6 +88,11 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe updateAndPost(); } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { updateAndPost(); @@ -120,6 +127,7 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe // Private methods. + @SuppressLint("SetTextI18n") private void updateAndPost() { textView.setText(getPlayerStateString() + getPlayerWindowIndexString() + getVideoString() + getAudioString()); @@ -130,16 +138,16 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe private String getPlayerStateString() { String text = "playWhenReady:" + player.getPlayWhenReady() + " playbackState:"; switch (player.getPlaybackState()) { - case ExoPlayer.STATE_BUFFERING: + case Player.STATE_BUFFERING: text += "buffering"; break; - case ExoPlayer.STATE_ENDED: + case Player.STATE_ENDED: text += "ended"; break; - case ExoPlayer.STATE_IDLE: + case Player.STATE_IDLE: text += "idle"; break; - case ExoPlayer.STATE_READY: + case Player.STATE_READY: text += "ready"; break; default: @@ -159,8 +167,8 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe return ""; } return "\n" + format.sampleMimeType + "(id:" + format.id + " r:" + format.width + "x" - + format.height + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) - + ")"; + + format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio) + + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; } private String getAudioString() { @@ -184,4 +192,9 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe + " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount; } + private static String getPixelAspectRatioString(float pixelAspectRatio) { + return pixelAspectRatio == Format.NO_VALUE || pixelAspectRatio == 1f ? "" + : (" par:" + String.format(Locale.US, "%.02f", pixelAspectRatio)); + } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 2b699c8957..523c7fd73d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -73,10 +73,11 @@ public class DefaultTimeBar extends View implements TimeBar { private final Rect bufferedBar; private final Rect scrubberBar; private final Paint playedPaint; - private final Paint scrubberPaint; private final Paint bufferedPaint; private final Paint unplayedPaint; private final Paint adMarkerPaint; + private final Paint playedAdMarkerPaint; + private final Paint scrubberPaint; private final int barHeight; private final int touchTargetHeight; private final int adMarkerWidth; @@ -101,8 +102,9 @@ public class DefaultTimeBar extends View implements TimeBar { private long duration; private long position; private long bufferedPosition; - private int adBreakCount; - private long[] adBreakTimesMs; + private int adGroupCount; + private long[] adGroupTimesMs; + private boolean[] playedAdGroups; /** * Creates a new time bar. @@ -114,10 +116,12 @@ public class DefaultTimeBar extends View implements TimeBar { bufferedBar = new Rect(); scrubberBar = new Rect(); playedPaint = new Paint(); - scrubberPaint = new Paint(); bufferedPaint = new Paint(); unplayedPaint = new Paint(); adMarkerPaint = new Paint(); + playedAdMarkerPaint = new Paint(); + scrubberPaint = new Paint(); + scrubberPaint.setAntiAlias(true); // Calculate the dimensions and paints for drawn elements. Resources res = context.getResources(); @@ -154,11 +158,14 @@ public class DefaultTimeBar extends View implements TimeBar { getDefaultUnplayedColor(playedColor)); int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR); + int playedAdMarkerColor = a.getInt(R.styleable.DefaultTimeBar_played_ad_marker_color, + getDefaultPlayedAdMarkerColor(adMarkerColor)); playedPaint.setColor(playedColor); scrubberPaint.setColor(scrubberColor); bufferedPaint.setColor(bufferedColor); unplayedPaint.setColor(unplayedColor); adMarkerPaint.setColor(adMarkerColor); + playedAdMarkerPaint.setColor(playedAdMarkerColor); } finally { a.recycle(); } @@ -237,10 +244,13 @@ public class DefaultTimeBar extends View implements TimeBar { } @Override - public void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount) { - Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null); - this.adBreakCount = adBreakCount; - this.adBreakTimesMs = adBreakTimesMs; + public void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, @Nullable boolean[] playedAdGroups, + int adGroupCount) { + Assertions.checkArgument(adGroupCount == 0 + || (adGroupTimesMs != null && playedAdGroups != null)); + this.adGroupCount = adGroupCount; + this.adGroupTimesMs = adGroupTimesMs; + this.playedAdGroups = playedAdGroups; update(); } @@ -517,13 +527,14 @@ public class DefaultTimeBar extends View implements TimeBar { canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint); } int adMarkerOffset = adMarkerWidth / 2; - for (int i = 0; i < adBreakCount; i++) { - long adBreakTimeMs = Util.constrainValue(adBreakTimesMs[i], 0, duration); + for (int i = 0; i < adGroupCount; i++) { + long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration); int markerPositionOffset = - (int) (progressBar.width() * adBreakTimeMs / duration) - adMarkerOffset; + (int) (progressBar.width() * adGroupTimeMs / duration) - adMarkerOffset; int markerLeft = progressBar.left + Math.min(progressBar.width() - adMarkerWidth, Math.max(0, markerPositionOffset)); - canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, adMarkerPaint); + Paint paint = playedAdGroups[i] ? playedAdMarkerPaint : adMarkerPaint; + canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, paint); } } @@ -589,4 +600,8 @@ public class DefaultTimeBar extends View implements TimeBar { return 0xCC000000 | (playedColor & 0x00FFFFFF); } + private static int getDefaultPlayedAdMarkerColor(int adMarkerColor) { + return 0x33000000 | (adMarkerColor & 0x00FFFFFF); + } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 250c237772..6ddbfed973 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -18,29 +18,35 @@ package com.google.android.exoplayer2.ui; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; +import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; import android.os.SystemClock; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.RepeatMode; 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 com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Formatter; import java.util.Locale; /** - * A view for controlling {@link ExoPlayer} instances. + * A view for controlling {@link Player} instances. *

    * A PlaybackControlView can be customized by setting attributes (or calling corresponding methods), * overriding the view's layout file or by specifying a custom view layout file, as outlined below. @@ -70,6 +76,14 @@ import java.util.Locale; *

  • Default: {@link #DEFAULT_FAST_FORWARD_MS}
  • * * + *
  • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat + * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, + * {@code all}, or {@code one|all}. + *
      + *
    • Corresponding method: {@link #setRepeatToggleModes(int)}
    • + *
    • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES}
    • + *
    + *
  • *
  • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See * below for more details. *
      @@ -117,6 +131,11 @@ import java.util.Locale; *
    • Type: {@link View}
    • *
    *
  • + *
  • {@code exo_repeat_toggle} - The repeat toggle button. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • *
  • {@code exo_position} - Text view displaying the current playback position. *
      *
    • Type: {@link TextView}
    • @@ -146,6 +165,10 @@ import java.util.Locale; */ public class PlaybackControlView extends FrameLayout { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); + } + /** * Listener to be notified about changes of the visibility of the UI control. */ @@ -161,7 +184,7 @@ public class PlaybackControlView extends FrameLayout { } /** - * Dispatches operations to the player. + * Dispatches operations to the {@link Player}. *

      * Implementations may choose to suppress (e.g. prevent playback from resuming if audio focus is * denied) or modify (e.g. change the seek position to prevent a user from seeking past a @@ -170,24 +193,33 @@ public class PlaybackControlView extends FrameLayout { public interface ControlDispatcher { /** - * Dispatches a {@link ExoPlayer#setPlayWhenReady(boolean)} operation. + * Dispatches a {@link Player#setPlayWhenReady(boolean)} operation. * - * @param player The player to which the operation should be dispatched. + * @param player The {@link Player} to which the operation should be dispatched. * @param playWhenReady Whether playback should proceed when ready. * @return True if the operation was dispatched. False if suppressed. */ - boolean dispatchSetPlayWhenReady(ExoPlayer player, boolean playWhenReady); + boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady); /** - * Dispatches a {@link ExoPlayer#seekTo(int, long)} operation. + * Dispatches a {@link Player#seekTo(int, long)} operation. * - * @param player The player to which the operation should be dispatched. + * @param player The {@link Player} to which the operation should be dispatched. * @param windowIndex The index of the window. * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek * to the window's default position. * @return True if the operation was dispatched. False if suppressed. */ - boolean dispatchSeekTo(ExoPlayer player, int windowIndex, long positionMs); + boolean dispatchSeekTo(Player player, int windowIndex, long positionMs); + + /** + * Dispatches a {@link Player#setRepeatMode(int)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param repeatMode The repeat mode. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode); } @@ -198,22 +230,42 @@ public class PlaybackControlView extends FrameLayout { public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new ControlDispatcher() { @Override - public boolean dispatchSetPlayWhenReady(ExoPlayer player, boolean playWhenReady) { + public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { player.setPlayWhenReady(playWhenReady); return true; } @Override - public boolean dispatchSeekTo(ExoPlayer player, int windowIndex, long positionMs) { + public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) { player.seekTo(windowIndex, positionMs); return true; } + @Override + public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + return true; + } + }; + /** + * The default fast forward increment, in milliseconds. + */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** + * The default rewind increment, in milliseconds. + */ public static final int DEFAULT_REWIND_MS = 5000; + /** + * The default show timeout, in milliseconds. + */ public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + /** + * The default repeat toggle modes. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; /** * The maximum number of windows that can be shown in a multi-window time bar. @@ -229,6 +281,7 @@ public class PlaybackControlView extends FrameLayout { private final View pauseButton; private final View fastForwardButton; private final View rewindButton; + private final ImageView repeatToggleButton; private final TextView durationView; private final TextView positionView; private final TimeBar timeBar; @@ -237,7 +290,14 @@ public class PlaybackControlView extends FrameLayout { private final Timeline.Period period; private final Timeline.Window window; - private ExoPlayer player; + private final Drawable repeatOffButtonDrawable; + private final Drawable repeatOneButtonDrawable; + private final Drawable repeatAllButtonDrawable; + private final String repeatOffButtonContentDescription; + private final String repeatOneButtonContentDescription; + private final String repeatAllButtonContentDescription; + + private Player player; private ControlDispatcher controlDispatcher; private VisibilityListener visibilityListener; @@ -248,8 +308,10 @@ public class PlaybackControlView extends FrameLayout { private int rewindMs; private int fastForwardMs; private int showTimeoutMs; + private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; private long hideAtMs; - private long[] adBreakTimesMs; + private long[] adGroupTimesMs; + private boolean[] playedAdGroups; private final Runnable updateProgressAction = new Runnable() { @Override @@ -280,6 +342,7 @@ public class PlaybackControlView extends FrameLayout { rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; + repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlaybackControlView, 0, 0); @@ -290,6 +353,7 @@ public class PlaybackControlView extends FrameLayout { showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); + repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); } finally { a.recycle(); } @@ -298,7 +362,8 @@ public class PlaybackControlView extends FrameLayout { window = new Timeline.Window(); formatBuilder = new StringBuilder(); formatter = new Formatter(formatBuilder, Locale.getDefault()); - adBreakTimesMs = new long[0]; + adGroupTimesMs = new long[0]; + playedAdGroups = new boolean[0]; componentListener = new ComponentListener(); controlDispatcher = DEFAULT_CONTROL_DISPATCHER; @@ -335,21 +400,42 @@ public class PlaybackControlView extends FrameLayout { if (fastForwardButton != null) { fastForwardButton.setOnClickListener(componentListener); } + repeatToggleButton = (ImageView) findViewById(R.id.exo_repeat_toggle); + if (repeatToggleButton != null) { + repeatToggleButton.setOnClickListener(componentListener); + } + Resources resources = context.getResources(); + repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); + repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); + repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); + repeatOffButtonContentDescription = resources.getString( + R.string.exo_controls_repeat_off_description); + repeatOneButtonContentDescription = resources.getString( + R.string.exo_controls_repeat_one_description); + repeatAllButtonContentDescription = resources.getString( + R.string.exo_controls_repeat_all_description); + } + + @SuppressWarnings("ResourceType") + private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(TypedArray a, + @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes); } /** - * Returns the player currently being controlled by this view, or null if no player is set. + * Returns the {@link Player} currently being controlled by this view, or null if no player is + * set. */ - public ExoPlayer getPlayer() { + public Player getPlayer() { return player; } /** - * Sets the {@link ExoPlayer} to control. + * Sets the {@link Player} to control. * - * @param player The {@code ExoPlayer} to control. + * @param player The {@link Player} to control. */ - public void setPlayer(ExoPlayer player) { + public void setPlayer(Player player) { if (this.player == player) { return; } @@ -440,6 +526,37 @@ public class PlaybackControlView extends FrameLayout { this.showTimeoutMs = showTimeoutMs; } + /** + * Returns which repeat toggle modes are enabled. + * + * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. + */ + public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { + return repeatToggleModes; + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + this.repeatToggleModes = repeatToggleModes; + if (player != null) { + @Player.RepeatMode int currentMode = player.getRepeatMode(); + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE + && currentMode != Player.REPEAT_MODE_OFF) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE + && currentMode == Player.REPEAT_MODE_ALL) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL + && currentMode == Player.REPEAT_MODE_ONE) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL); + } + } + } + /** * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will * be automatically hidden after this duration of time has elapsed without user input. @@ -494,6 +611,7 @@ public class PlaybackControlView extends FrameLayout { private void updateAll() { updatePlayPauseButton(); updateNavigation(); + updateRepeatModeButton(); updateProgress(); } @@ -529,9 +647,10 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = windowIndex > 0 || isSeekable || !window.isDynamic; - enableNext = (windowIndex < timeline.getWindowCount() - 1) || window.isDynamic; - if (timeline.getPeriod(player.getCurrentPeriodIndex(), period).isAd) { + enablePrevious = !timeline.isFirstWindow(windowIndex, player.getRepeatMode()) + || isSeekable || !window.isDynamic; + enableNext = !timeline.isLastWindow(windowIndex, player.getRepeatMode()) || window.isDynamic; + if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); } @@ -545,12 +664,42 @@ public class PlaybackControlView extends FrameLayout { } } + private void updateRepeatModeButton() { + if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { + return; + } + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { + repeatToggleButton.setVisibility(View.GONE); + return; + } + if (player == null) { + setButtonEnabled(false, repeatToggleButton); + return; + } + setButtonEnabled(true, repeatToggleButton); + switch (player.getRepeatMode()) { + case Player.REPEAT_MODE_OFF: + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); + break; + case Player.REPEAT_MODE_ONE: + repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); + repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); + break; + case Player.REPEAT_MODE_ALL: + repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); + repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); + break; + } + repeatToggleButton.setVisibility(View.VISIBLE); + } + private void updateTimeBarMode() { if (player == null) { return; } multiWindowTimeBar = showMultiWindowTimeBar - && canShowMultiWindowTimeBar(player.getCurrentTimeline(), period); + && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); } private void updateProgress() { @@ -562,59 +711,64 @@ public class PlaybackControlView extends FrameLayout { long bufferedPosition = 0; long duration = 0; if (player != null) { - if (multiWindowTimeBar) { - Timeline timeline = player.getCurrentTimeline(); - int windowCount = timeline.getWindowCount(); - int periodIndex = player.getCurrentPeriodIndex(); - long positionUs = 0; - long bufferedPositionUs = 0; - long durationUs = 0; - boolean isInAdBreak = false; - boolean isPlayingAd = false; - int adBreakCount = 0; - for (int i = 0; i < windowCount; i++) { + long currentWindowTimeBarOffsetUs = 0; + long durationUs = 0; + int adGroupCount = 0; + Timeline timeline = player.getCurrentTimeline(); + if (!timeline.isEmpty()) { + int currentWindowIndex = player.getCurrentWindowIndex(); + int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; + int lastWindowIndex = + multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; + for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { + if (i == currentWindowIndex) { + currentWindowTimeBarOffsetUs = durationUs; + } timeline.getWindow(i, window); + if (window.durationUs == C.TIME_UNSET) { + Assertions.checkState(!multiWindowTimeBar); + break; + } for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - if (timeline.getPeriod(j, period).isAd) { - isPlayingAd |= j == periodIndex; - if (!isInAdBreak) { - isInAdBreak = true; - if (adBreakCount == adBreakTimesMs.length) { - adBreakTimesMs = Arrays.copyOf(adBreakTimesMs, - adBreakTimesMs.length == 0 ? 1 : adBreakTimesMs.length * 2); + timeline.getPeriod(j, period); + int periodAdGroupCount = period.getAdGroupCount(); + for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) { + long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); + if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { + if (period.durationUs == C.TIME_UNSET) { + // Don't show ad markers for postrolls in periods with unknown duration. + continue; } - adBreakTimesMs[adBreakCount++] = C.usToMs(durationUs); + adGroupTimeInPeriodUs = period.durationUs; } - } else { - isInAdBreak = false; - long periodDurationUs = period.getDurationUs(); - Assertions.checkState(periodDurationUs != C.TIME_UNSET); - long periodDurationInWindowUs = periodDurationUs; - if (j == window.firstPeriodIndex) { - periodDurationInWindowUs -= window.positionInFirstPeriodUs; + long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); + if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupCount == adGroupTimesMs.length) { + int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); + playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); + } + adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs); + playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); + adGroupCount++; } - if (i < periodIndex) { - positionUs += periodDurationInWindowUs; - bufferedPositionUs += periodDurationInWindowUs; - } - durationUs += periodDurationInWindowUs; } } + durationUs += window.durationUs; } - position = C.usToMs(positionUs); - bufferedPosition = C.usToMs(bufferedPositionUs); - duration = C.usToMs(durationUs); - if (!isPlayingAd) { - position += player.getCurrentPosition(); - bufferedPosition += player.getBufferedPosition(); - } - if (timeBar != null) { - timeBar.setAdBreakTimesMs(adBreakTimesMs, adBreakCount); - } + } + duration = C.usToMs(durationUs); + position = C.usToMs(currentWindowTimeBarOffsetUs); + bufferedPosition = position; + if (player.isPlayingAd()) { + position += player.getContentPosition(); + bufferedPosition = position; } else { - position = player.getCurrentPosition(); - bufferedPosition = player.getBufferedPosition(); - duration = player.getDuration(); + position += player.getCurrentPosition(); + bufferedPosition += player.getBufferedPosition(); + } + if (timeBar != null) { + timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, adGroupCount); } } if (durationView != null) { @@ -631,10 +785,10 @@ public class PlaybackControlView extends FrameLayout { // Cancel any pending updates and schedule a new one if necessary. removeCallbacks(updateProgressAction); - int playbackState = player == null ? ExoPlayer.STATE_IDLE : player.getPlaybackState(); - if (playbackState != ExoPlayer.STATE_IDLE && playbackState != ExoPlayer.STATE_ENDED) { + int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); + if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { long delayMs; - if (player.getPlayWhenReady() && playbackState == ExoPlayer.STATE_READY) { + if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { delayMs = 1000 - (position % 1000); if (delayMs < 200) { delayMs += 1000; @@ -680,9 +834,11 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - if (windowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { - seekTo(windowIndex - 1, C.TIME_UNSET); + seekTo(previousWindowIndex, C.TIME_UNSET); } else { seekTo(0); } @@ -694,8 +850,9 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - if (windowIndex < timeline.getWindowCount() - 1) { - seekTo(windowIndex + 1, C.TIME_UNSET); + int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()); + if (nextWindowIndex != C.INDEX_UNSET) { + seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { seekTo(windowIndex, C.TIME_UNSET); } @@ -733,40 +890,28 @@ public class PlaybackControlView extends FrameLayout { } } - private void seekToTimebarPosition(long timebarPositionMs) { - if (multiWindowTimeBar) { - Timeline timeline = player.getCurrentTimeline(); + private void seekToTimeBarPosition(long positionMs) { + int windowIndex; + Timeline timeline = player.getCurrentTimeline(); + if (multiWindowTimeBar && !timeline.isEmpty()) { int windowCount = timeline.getWindowCount(); - long remainingMs = timebarPositionMs; - for (int i = 0; i < windowCount; i++) { - timeline.getWindow(i, window); - for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - if (!timeline.getPeriod(j, period).isAd) { - long periodDurationMs = period.getDurationMs(); - if (periodDurationMs == C.TIME_UNSET) { - // Should never happen as canShowMultiWindowTimeBar is true. - throw new IllegalStateException(); - } - if (j == window.firstPeriodIndex) { - periodDurationMs -= window.getPositionInFirstPeriodMs(); - } - if (i == windowCount - 1 && j == window.lastPeriodIndex - && remainingMs >= periodDurationMs) { - // Seeking past the end of the last window should seek to the end of the timeline. - seekTo(i, window.getDurationMs()); - return; - } - if (remainingMs < periodDurationMs) { - seekTo(i, period.getPositionInWindowMs() + remainingMs); - return; - } - remainingMs -= periodDurationMs; - } + windowIndex = 0; + while (true) { + long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); + if (positionMs < windowDurationMs) { + break; + } else if (windowIndex == windowCount - 1) { + // Seeking past the end of the last window should seek to the end of the timeline. + positionMs = windowDurationMs; + break; } + positionMs -= windowDurationMs; + windowIndex++; } } else { - seekTo(timebarPositionMs); + windowIndex = player.getCurrentWindowIndex(); } + seekTo(windowIndex, positionMs); } @Override @@ -794,11 +939,7 @@ public class PlaybackControlView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { - boolean handled = dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - if (handled) { - show(); - } - return handled; + return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } /** @@ -840,7 +981,6 @@ public class PlaybackControlView extends FrameLayout { break; } } - show(); return true; } @@ -859,24 +999,23 @@ public class PlaybackControlView extends FrameLayout { * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. * * @param timeline The {@link Timeline} to check. - * @param period A scratch {@link Timeline.Period} instance. + * @param window A scratch {@link Timeline.Window} instance. * @return Whether the specified timeline can be shown on a multi-window time bar. */ - private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Period period) { + private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { return false; } - int periodCount = timeline.getPeriodCount(); - for (int i = 0; i < periodCount; i++) { - timeline.getPeriod(i, period); - if (!period.isAd && period.durationUs == C.TIME_UNSET) { + int windowCount = timeline.getWindowCount(); + for (int i = 0; i < windowCount; i++) { + if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { return false; } } return true; } - private final class ComponentListener implements ExoPlayer.EventListener, TimeBar.OnScrubListener, + private final class ComponentListener implements Player.EventListener, TimeBar.OnScrubListener, OnClickListener { @Override @@ -896,7 +1035,7 @@ public class PlaybackControlView extends FrameLayout { public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { scrubbing = false; if (!canceled && player != null) { - seekToTimebarPosition(position); + seekToTimeBarPosition(position); } hideAfterTimeout(); } @@ -907,6 +1046,12 @@ public class PlaybackControlView extends FrameLayout { updateProgress(); } + @Override + public void onRepeatModeChanged(int repeatMode) { + updateRepeatModeButton(); + updateNavigation(); + } + @Override public void onPositionDiscontinuity() { updateNavigation(); @@ -955,6 +1100,9 @@ public class PlaybackControlView extends FrameLayout { controlDispatcher.dispatchSetPlayWhenReady(player, true); } else if (pauseButton == view) { controlDispatcher.dispatchSetPlayWhenReady(player, false); + } else if (repeatToggleButton == view) { + controlDispatcher.dispatchSetRepeatMode(player, RepeatModeUtil.getNextRepeatMode( + player.getRepeatMode(), repeatToggleModes)); } } hideAfterTimeout(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 245999f8b5..2bba9071fd 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -35,8 +35,8 @@ import android.widget.FrameLayout; import android.widget.ImageView; 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.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.metadata.Metadata; @@ -49,6 +49,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; import com.google.android.exoplayer2.ui.PlaybackControlView.ControlDispatcher; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; import java.util.List; @@ -88,6 +89,14 @@ import java.util.List; *

    • Default: {@code true}
    • *
    *
  • + *
  • {@code auto_show} - Whether the playback controls are automatically shown when + * playback starts, pauses, ends, or fails. If set to false, the playback controls can be + * manually operated with {@link #showController()} and {@link #hideController()}. + *
      + *
    • Corresponding method: {@link #setControllerAutoShow(boolean)}
    • + *
    • Default: {@code true}
    • + *
    + *
  • *
  • {@code resize_mode} - Controls how video and album art is resized within the view. * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. *
      @@ -117,7 +126,8 @@ import java.util.List; *
    • Default: {@code R.id.exo_playback_control_view}
    • *
    *
  • All attributes that can be set on a {@link PlaybackControlView} can also be set on a - * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView}. + * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} + * unless the layout is overridden to specify a custom {@code exo_controller} (see below). *
  • * * @@ -154,11 +164,19 @@ import java.util.List; * * *
  • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated - * {@link PlaybackControlView}. + * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. *
      *
    • Type: {@link View}
    • *
    *
  • + *
  • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use + * of a custom extension of {@link PlaybackControlView}. Note that attributes such as + * {@code rewind_increment} will not be automatically propagated through to this instance. If + * a view exists with this id, any {@code exo_controller_placeholder} view will be ignored. + *
      + *
    • Type: {@link PlaybackControlView}
    • + *
    + *
  • *
  • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. *
      @@ -198,6 +216,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private boolean useArtwork; private Bitmap defaultArtwork; private int controllerShowTimeoutMs; + private boolean controllerAutoShow; private boolean controllerHideOnTouch; public SimpleExoPlayerView(Context context) { @@ -238,6 +257,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; boolean controllerHideOnTouch = true; + boolean controllerAutoShow = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -254,6 +274,8 @@ public final class SimpleExoPlayerView extends FrameLayout { controllerShowTimeoutMs); controllerHideOnTouch = a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, controllerHideOnTouch); + controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, + controllerAutoShow); } finally { a.recycle(); } @@ -302,8 +324,11 @@ public final class SimpleExoPlayerView extends FrameLayout { } // Playback control view. + PlaybackControlView customController = (PlaybackControlView) findViewById(R.id.exo_controller); View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); - if (controllerPlaceholder != null) { + if (customController != null) { + this.controller = customController; + } else if (controllerPlaceholder != null) { // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit // calls to set them. this.controller = new PlaybackControlView(context, attrs); @@ -317,6 +342,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; this.controllerHideOnTouch = controllerHideOnTouch; + this.controllerAutoShow = controllerAutoShow; this.useController = useController && controller != null; hideController(); } @@ -480,6 +506,12 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + maybeShowController(true); + return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + /** * Called to process media key events. Any {@link KeyEvent} can be passed but only media key * events will be handled. Does nothing if playback controls are disabled. @@ -493,11 +525,13 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Shows the playback controls. Does nothing if playback controls are disabled. + * + *

      The playback controls are automatically hidden during playback after + * {{@link #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not + * started yet, is paused, has ended or failed. */ public void showController() { - if (useController) { - maybeShowController(true); - } + showController(shouldShowControllerIndefinitely()); } /** @@ -550,6 +584,26 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerHideOnTouch = controllerHideOnTouch; } + /** + * Returns whether the playback controls are automatically shown when playback starts, pauses, + * ends, or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + */ + public boolean getControllerAutoShow() { + return controllerAutoShow; + } + + /** + * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, + * or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + * + * @param controllerAutoShow Whether the playback controls are allowed to show automatically. + */ + public void setControllerAutoShow(boolean controllerAutoShow) { + this.controllerAutoShow = controllerAutoShow; + } + /** * Set the {@link PlaybackControlView.VisibilityListener}. * @@ -593,6 +647,16 @@ public final class SimpleExoPlayerView extends FrameLayout { controller.setFastForwardIncrementMs(fastForwardMs); } + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + Assertions.checkState(controller != null); + controller.setRepeatToggleModes(repeatToggleModes); + } + /** * Sets whether the time bar should show all windows, as opposed to just the current one. * @@ -656,18 +720,34 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } + /** + * Shows the playback controls, but only if forced or shown indefinitely. + */ private void maybeShowController(boolean isForced) { - if (!useController || player == null) { - return; + if (useController) { + boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; + boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); + if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { + showController(shouldShowIndefinitely); + } + } + } + + private boolean shouldShowControllerIndefinitely() { + if (player == null) { + return true; } int playbackState = player.getPlaybackState(); - boolean showIndefinitely = playbackState == ExoPlayer.STATE_IDLE - || playbackState == ExoPlayer.STATE_ENDED || !player.getPlayWhenReady(); - boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; - controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); - if (isForced || showIndefinitely || wasShowingIndefinitely) { - controller.show(); + return controllerAutoShow && (playbackState == Player.STATE_IDLE + || playbackState == Player.STATE_ENDED || !player.getPlayWhenReady()); + } + + private void showController(boolean showIndefinitely) { + if (!useController) { + return; } + controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); + controller.show(); } private void updateForCurrentTrackSelections() { @@ -762,7 +842,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } private final class ComponentListener implements SimpleExoPlayer.VideoListener, - TextRenderer.Output, ExoPlayer.EventListener { + TextRenderer.Output, Player.EventListener { // TextRenderer.Output implementation @@ -796,7 +876,7 @@ public final class SimpleExoPlayerView extends FrameLayout { updateForCurrentTrackSelections(); } - // ExoPlayer.EventListener implementation + // Player.EventListener implementation @Override public void onLoadingChanged(boolean isLoading) { @@ -808,6 +888,11 @@ public final class SimpleExoPlayerView extends FrameLayout { maybeShowController(false); } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException e) { // Do nothing. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java index 2fd5bff5eb..4b448738d3 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -78,13 +78,17 @@ public interface TimeBar { void setDuration(long duration); /** - * Sets the times of ad breaks. + * Sets the times of ad groups and whether each ad group has been played. * - * @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of - * ad breaks in milliseconds. May be {@code null} if there are no ad breaks. - * @param adBreakCount The number of ad breaks. + * @param adGroupTimesMs An array where the first {@code adGroupCount} elements are the times of + * ad groups in milliseconds. May be {@code null} if there are no ad groups. + * @param playedAdGroups An array where the first {@code adGroupCount} elements indicate whether + * the corresponding ad groups have been played. May be {@code null} if there are no ad + * groups. + * @param adGroupCount The number of ad groups. */ - void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); + void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, @Nullable boolean[] playedAdGroups, + int adGroupCount); /** * Listener for scrubbing events. diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml new file mode 100644 index 0000000000..dad37fa1f0 --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_all.xml @@ -0,0 +1,23 @@ + + + + diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml new file mode 100644 index 0000000000..132eae0d76 --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_off.xml @@ -0,0 +1,23 @@ + + + + diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml new file mode 100644 index 0000000000..d51010566a --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_repeat_one.xml @@ -0,0 +1,23 @@ + + + + diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000..2824e7847c Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_all.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_off.png new file mode 100644 index 0000000000..0b92f583da Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_off.png differ diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_one.png new file mode 100644 index 0000000000..232aa2b1cd Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_controls_repeat_one.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000..5c91a47519 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_all.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_off.png new file mode 100644 index 0000000000..a94abd864f Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_off.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_one.png new file mode 100644 index 0000000000..a59a985239 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_controls_repeat_one.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000..97f7e1cc75 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_all.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png new file mode 100644 index 0000000000..6a02321702 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_off.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png new file mode 100644 index 0000000000..59bac33705 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_controls_repeat_one.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000..2baaedecbf Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_all.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_off.png new file mode 100644 index 0000000000..2468f92f9f Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_off.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_one.png new file mode 100644 index 0000000000..4e1d53db77 Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_controls_repeat_one.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png new file mode 100644 index 0000000000..d7207ebc0d Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_all.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png new file mode 100644 index 0000000000..4d6253ead6 Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_off.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png new file mode 100644 index 0000000000..d577f4ebcd Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_repeat_one.png differ diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index 1d6267e7f0..407329890d 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -34,6 +34,9 @@ + + diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 9f1bce53d9..103877f1e6 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -22,4 +22,7 @@ "Stop" "Spoel terug" "Vinnig vorentoe" + "Herhaal alles" + "Herhaal niks" + "Herhaal een" diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index f06c2a664e..356566cb87 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -22,4 +22,7 @@ "አቁም" "ወደኋላ አጠንጥን" "በፍጥነት አሳልፍ" + "ሁሉንም ድገም" + "ምንም አትድገም" + "አንዱን ድገም" diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index a40c961bf7..4bdbda061c 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -22,4 +22,7 @@ "إيقاف" "إرجاع" "تقديم سريع" + "تكرار الكل" + "عدم التكرار" + "تكرار مقطع واحد" diff --git a/library/ui/src/main/res/values-az-rAZ/strings.xml b/library/ui/src/main/res/values-az-rAZ/strings.xml index 7b3b9366b5..771335952f 100644 --- a/library/ui/src/main/res/values-az-rAZ/strings.xml +++ b/library/ui/src/main/res/values-az-rAZ/strings.xml @@ -22,4 +22,7 @@ "Dayandır" "Geri sarıma" "Sürətlə irəli" + "Bütün təkrarlayın" + "Təkrar bir" + "Heç bir təkrar" diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml index b5fdd74402..7c373b5b55 100644 --- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -22,4 +22,7 @@ "Zaustavi" "Premotaj unazad" "Premotaj unapred" + "Ponovi sve" + "Ne ponavljaj nijednu" + "Ponovi jednu" diff --git a/library/ui/src/main/res/values-be-rBY/strings.xml b/library/ui/src/main/res/values-be-rBY/strings.xml index 890c23ebd5..7790a7887f 100644 --- a/library/ui/src/main/res/values-be-rBY/strings.xml +++ b/library/ui/src/main/res/values-be-rBY/strings.xml @@ -22,4 +22,7 @@ "Спыніць" "Перамотка назад" "Перамотка ўперад" + "Паўтарыць усё" + "Паўтараць ні" + "Паўтарыць адзін" diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index 30b905fb8e..ce9e3d6943 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -22,4 +22,7 @@ "Спиране" "Превъртане назад" "Превъртане напред" + "Повтаряне на всички" + "Без повтаряне" + "Повтаряне на един елемент" diff --git a/library/ui/src/main/res/values-bn-rBD/strings.xml b/library/ui/src/main/res/values-bn-rBD/strings.xml index ca5d9461d3..5f8ebfa98e 100644 --- a/library/ui/src/main/res/values-bn-rBD/strings.xml +++ b/library/ui/src/main/res/values-bn-rBD/strings.xml @@ -22,4 +22,7 @@ "থামান" "গুটিয়ে নিন" "দ্রুত সামনে এগোন" + "সবগুলির পুনরাবৃত্তি করুন" + "একটিরও পুনরাবৃত্তি করবেন না" + "একটির পুনরাবৃত্তি করুন" diff --git a/library/ui/src/main/res/values-bs-rBA/strings.xml b/library/ui/src/main/res/values-bs-rBA/strings.xml index 9cb0ca4d76..ef47099760 100644 --- a/library/ui/src/main/res/values-bs-rBA/strings.xml +++ b/library/ui/src/main/res/values-bs-rBA/strings.xml @@ -22,4 +22,7 @@ "Zaustavi" "Premotaj" "Ubrzaj" + "Ponovite sve" + "Ne ponavljaju" + "Ponovite jedan" diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index 0816c76b12..a42fe3b9cb 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -22,4 +22,7 @@ "Atura" "Rebobina" "Avança ràpidament" + "Repeteix-ho tot" + "No en repeteixis cap" + "Repeteix-ne un" diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 22cff4041e..9c1e50ce27 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -22,4 +22,7 @@ "Zastavit" "Přetočit zpět" "Přetočit vpřed" + "Opakovat vše" + "Neopakovat" + "Opakovat jednu položku" diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index a6710bea50..3ec132ebb7 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -22,4 +22,7 @@ "Stop" "Spol tilbage" "Spol frem" + "Gentag alle" + "Gentag ingen" + "Gentag en" diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index cdfd2d4baf..a1dc749864 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -22,4 +22,7 @@ "Beenden" "Zurückspulen" "Vorspulen" + "Alle wiederholen" + "Keinen Titel wiederholen" + "Einen Titel wiederholen" diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 1e11df3b14..845011fe55 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -22,4 +22,7 @@ "Διακοπή" "Επαναφορά" "Γρήγορη προώθηση" + "Επανάληψη όλων" + "Καμία επανάληψη" + "Επανάληψη ενός στοιχείου" diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml index 5077cf2b94..8a1742c8ca 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -22,4 +22,7 @@ "Stop" "Rewind" "Fast-forward" + "Repeat all" + "Repeat none" + "Repeat one" diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml index 5077cf2b94..8a1742c8ca 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -22,4 +22,7 @@ "Stop" "Rewind" "Fast-forward" + "Repeat all" + "Repeat none" + "Repeat one" diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index 5077cf2b94..8a1742c8ca 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -22,4 +22,7 @@ "Stop" "Rewind" "Fast-forward" + "Repeat all" + "Repeat none" + "Repeat one" diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index 72b176e538..f2ec848fb6 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -22,4 +22,7 @@ "Detener" "Retroceder" "Avanzar" + "Repetir todo" + "No repetir" + "Repetir uno" diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 3b188d266d..116f064223 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -22,4 +22,7 @@ "Detener" "Rebobinar" "Avance rápido" + "Repetir todo" + "No repetir" + "Repetir uno" diff --git a/library/ui/src/main/res/values-et-rEE/strings.xml b/library/ui/src/main/res/values-et-rEE/strings.xml index 7a01bd9d5a..153611ece4 100644 --- a/library/ui/src/main/res/values-et-rEE/strings.xml +++ b/library/ui/src/main/res/values-et-rEE/strings.xml @@ -22,4 +22,7 @@ "Peata" "Keri tagasi" "Keri edasi" + "Korda kõike" + "Ära korda midagi" + "Korda ühte" diff --git a/library/ui/src/main/res/values-eu-rES/strings.xml b/library/ui/src/main/res/values-eu-rES/strings.xml index 3dd51d2138..1128572d9a 100644 --- a/library/ui/src/main/res/values-eu-rES/strings.xml +++ b/library/ui/src/main/res/values-eu-rES/strings.xml @@ -22,4 +22,7 @@ "Gelditu" "Atzeratu" "Aurreratu" + "Errepikatu guztiak" + "Ez errepikatu" + "Errepikatu bat" diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index a8955ca2f3..d6be77323b 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -22,4 +22,7 @@ "توقف" "عقب بردن" "جلو بردن سریع" + "تکرار همه" + "تکرار هیچ‌کدام" + "یک‌بار تکرار" diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 5f1352d1af..10e4b0bbe3 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -22,4 +22,7 @@ "Seis" "Kelaa taakse" "Kelaa eteen" + "Toista kaikki" + "Toista ei mitään" + "Toista yksi" diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml index 51ba11e0c0..d8852b5d3f 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -22,4 +22,7 @@ "Arrêter" "Reculer" "Avance rapide" + "Tout lire en boucle" + "Aucune répétition" + "Répéter un élément" diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index d55b32b6f7..acf3670fa4 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -22,4 +22,7 @@ "Arrêter" "Retour arrière" "Avance rapide" + "Tout lire en boucle" + "Ne rien lire en boucle" + "Lire en boucle un élément" diff --git a/library/ui/src/main/res/values-gl-rES/strings.xml b/library/ui/src/main/res/values-gl-rES/strings.xml index 99ae59c7f9..81b854cafe 100644 --- a/library/ui/src/main/res/values-gl-rES/strings.xml +++ b/library/ui/src/main/res/values-gl-rES/strings.xml @@ -22,4 +22,7 @@ "Deter" "Rebobinar" "Avance rápido" + "Repetir todo" + "Non repetir" + "Repetir un" diff --git a/library/ui/src/main/res/values-gu-rIN/strings.xml b/library/ui/src/main/res/values-gu-rIN/strings.xml index 6feab0a3a6..6d51c29f97 100644 --- a/library/ui/src/main/res/values-gu-rIN/strings.xml +++ b/library/ui/src/main/res/values-gu-rIN/strings.xml @@ -22,4 +22,7 @@ "રોકો" "રીવાઇન્ડ કરો" "ઝડપી ફોરવર્ડ કરો" + "બધા પુનરાવર્તન કરો" + "કંઈ પુનરાવર્તન કરો" + "એક પુનરાવર્તન કરો" diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index 5229b67d0e..eadb0519df 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -22,4 +22,7 @@ "बंद करें" "रिवाइंड करें" "फ़ास्ट फ़ॉरवर्ड" + "सभी को दोहराएं" + "कुछ भी न दोहराएं" + "एक दोहराएं" diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index c0b075edde..cb49965640 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -22,4 +22,7 @@ "Zaustavi" "Unatrag" "Brzo unaprijed" + "Ponovi sve" + "Bez ponavljanja" + "Ponovi jedno" diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index 2a34684edb..43ac8f51ff 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -22,4 +22,7 @@ "Leállítás" "Visszatekerés" "Előretekerés" + "Összes ismétlése" + "Nincs ismétlés" + "Egy ismétlése" diff --git a/library/ui/src/main/res/values-hy-rAM/strings.xml b/library/ui/src/main/res/values-hy-rAM/strings.xml index 05f9d04ab7..3b09f9a507 100644 --- a/library/ui/src/main/res/values-hy-rAM/strings.xml +++ b/library/ui/src/main/res/values-hy-rAM/strings.xml @@ -22,4 +22,7 @@ "Դադարեցնել" "Հետ փաթաթել" "Արագ առաջ անցնել" + "կրկնել այն ամենը" + "Չկրկնել" + "Կրկնել մեկը" diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 062933a0a8..928be5945a 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -22,4 +22,7 @@ "Berhenti" "Putar Ulang" "Maju cepat" + "Ulangi Semua" + "Jangan Ulangi" + "Ulangi Satu" diff --git a/library/ui/src/main/res/values-is-rIS/strings.xml b/library/ui/src/main/res/values-is-rIS/strings.xml index 9c4421a272..75be2aeb17 100644 --- a/library/ui/src/main/res/values-is-rIS/strings.xml +++ b/library/ui/src/main/res/values-is-rIS/strings.xml @@ -22,4 +22,7 @@ "Stöðva" "Spóla til baka" "Spóla áfram" + "Endurtaka allt" + "Endurtaka ekkert" + "Endurtaka eitt" diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index 71525a2b3e..59117a6b75 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -22,4 +22,7 @@ "Interrompi" "Riavvolgi" "Avanti veloce" + "Ripeti tutti" + "Non ripetere nessuno" + "Ripeti uno" diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index f33cc2adb0..347b137cf2 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -22,4 +22,7 @@ "הפסק" "הרץ אחורה" "הרץ קדימה" + "חזור על הכל" + "אל תחזור על כלום" + "חזור על פריט אחד" diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index baa459aeca..cf2cc49b67 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -22,4 +22,7 @@ "停止" "巻き戻し" "早送り" + "全曲を繰り返し" + "繰り返しなし" + "1曲を繰り返し" diff --git a/library/ui/src/main/res/values-ka-rGE/strings.xml b/library/ui/src/main/res/values-ka-rGE/strings.xml index 5b87f86c34..75da8dde18 100644 --- a/library/ui/src/main/res/values-ka-rGE/strings.xml +++ b/library/ui/src/main/res/values-ka-rGE/strings.xml @@ -22,4 +22,7 @@ "შეწყვეტა" "უკან გადახვევა" "წინ გადახვევა" + "გამეორება ყველა" + "გაიმეორეთ არცერთი" + "გაიმეორეთ ერთი" diff --git a/library/ui/src/main/res/values-kk-rKZ/strings.xml b/library/ui/src/main/res/values-kk-rKZ/strings.xml index c1bf5c8b4b..b1ab22ecf6 100644 --- a/library/ui/src/main/res/values-kk-rKZ/strings.xml +++ b/library/ui/src/main/res/values-kk-rKZ/strings.xml @@ -22,4 +22,7 @@ "Тоқтату" "Кері айналдыру" "Жылдам алға айналдыру" + "Барлығын қайталау" + "Ешқайсысын қайталамау" + "Біреуін қайталау" diff --git a/library/ui/src/main/res/values-km-rKH/strings.xml b/library/ui/src/main/res/values-km-rKH/strings.xml index dbeeab60a6..dfd9f7d863 100644 --- a/library/ui/src/main/res/values-km-rKH/strings.xml +++ b/library/ui/src/main/res/values-km-rKH/strings.xml @@ -22,4 +22,7 @@ "បញ្ឈប់" "ខា​ថយក្រោយ" "ទៅ​មុខ​​​រហ័ស" + "ធ្វើ​ម្ដង​ទៀត​ទាំងអស់" + "មិន​ធ្វើ​ឡើង​វិញ" + "ធ្វើ​​ឡើងវិញ​ម្ដង" diff --git a/library/ui/src/main/res/values-kn-rIN/strings.xml b/library/ui/src/main/res/values-kn-rIN/strings.xml index b73cf0fdb0..868af17a65 100644 --- a/library/ui/src/main/res/values-kn-rIN/strings.xml +++ b/library/ui/src/main/res/values-kn-rIN/strings.xml @@ -22,4 +22,7 @@ "ನಿಲ್ಲಿಸು" "ರಿವೈಂಡ್ ಮಾಡು" "ವೇಗವಾಗಿ ಮುಂದಕ್ಕೆ" + "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ" + "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ" + "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ" diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 7097e2d9f7..89636ac8a0 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -22,4 +22,7 @@ "중지" "되감기" "빨리 감기" + "전체 반복" + "반복 안함" + "한 항목 반복" diff --git a/library/ui/src/main/res/values-ky-rKG/strings.xml b/library/ui/src/main/res/values-ky-rKG/strings.xml index 7090c178c3..15fd50468a 100644 --- a/library/ui/src/main/res/values-ky-rKG/strings.xml +++ b/library/ui/src/main/res/values-ky-rKG/strings.xml @@ -22,4 +22,7 @@ "Токтотуу" "Артка түрүү" "Алдыга түрүү" + "Баарын кайталоо" + "Эч бирин кайталабоо" + "Бирөөнү кайталоо" diff --git a/library/ui/src/main/res/values-lo-rLA/strings.xml b/library/ui/src/main/res/values-lo-rLA/strings.xml index 44095e4323..405d0c64fe 100644 --- a/library/ui/src/main/res/values-lo-rLA/strings.xml +++ b/library/ui/src/main/res/values-lo-rLA/strings.xml @@ -22,4 +22,7 @@ "ຢຸດ" "​ຣີ​​ວາຍກັບ" "ເລື່ອນ​ໄປ​ໜ້າ" + "ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ" + "​ບໍ່ຫຼິ້ນ​ຊ້ຳ" + "ຫຼິ້ນ​ຊ້ຳ" diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index 138caec322..bd7d4142fc 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -22,4 +22,7 @@ "Stabdyti" "Sukti atgal" "Sukti pirmyn" + "Kartoti viską" + "Nekartoti nieko" + "Kartoti vieną" diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml index 4c91da86cc..c2ebc70cbd 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -22,4 +22,7 @@ "Apturēt" "Attīt atpakaļ" "Ātri patīt" + "Atkārtot visu" + "Neatkārtot nevienu" + "Atkārtot vienu" diff --git a/library/ui/src/main/res/values-mk-rMK/strings.xml b/library/ui/src/main/res/values-mk-rMK/strings.xml index e9fedf689f..14ce7111a4 100644 --- a/library/ui/src/main/res/values-mk-rMK/strings.xml +++ b/library/ui/src/main/res/values-mk-rMK/strings.xml @@ -22,4 +22,7 @@ "Запри" "Премотај назад" "Брзо премотај напред" + "Повтори ги сите" + "Не повторувај ниту една" + "Повтори една" diff --git a/library/ui/src/main/res/values-ml-rIN/strings.xml b/library/ui/src/main/res/values-ml-rIN/strings.xml index acc33934fb..17fe7a1655 100644 --- a/library/ui/src/main/res/values-ml-rIN/strings.xml +++ b/library/ui/src/main/res/values-ml-rIN/strings.xml @@ -22,4 +22,7 @@ "നിര്‍ത്തുക" "റിവൈൻഡുചെയ്യുക" "വേഗത്തിലുള്ള കൈമാറൽ" + "എല്ലാം ആവർത്തിക്കുക" + "ഒന്നും ആവർത്തിക്കരുത്" + "ഒന്ന് ആവർത്തിക്കുക" diff --git a/library/ui/src/main/res/values-mn-rMN/strings.xml b/library/ui/src/main/res/values-mn-rMN/strings.xml index 6434e9ea16..bf9a7e03bf 100644 --- a/library/ui/src/main/res/values-mn-rMN/strings.xml +++ b/library/ui/src/main/res/values-mn-rMN/strings.xml @@ -22,4 +22,7 @@ "Зогсоох" "Буцааж хураах" "Хурдан урагшлуулах" + "Бүгдийг давтах" + "Алийг нь ч давтахгүй" + "Нэгийг давтах" diff --git a/library/ui/src/main/res/values-mr-rIN/strings.xml b/library/ui/src/main/res/values-mr-rIN/strings.xml index 8f4d0d75b1..df4ac9de6b 100644 --- a/library/ui/src/main/res/values-mr-rIN/strings.xml +++ b/library/ui/src/main/res/values-mr-rIN/strings.xml @@ -22,4 +22,7 @@ "थांबा" "रिवाईँड करा" "फास्ट फॉरवर्ड करा" + "सर्व पुनरावृत्ती करा" + "काहीही पुनरावृत्ती करू नका" + "एक पुनरावृत्ती करा" diff --git a/library/ui/src/main/res/values-ms-rMY/strings.xml b/library/ui/src/main/res/values-ms-rMY/strings.xml index 91f74bbc1c..33dfcb40f0 100644 --- a/library/ui/src/main/res/values-ms-rMY/strings.xml +++ b/library/ui/src/main/res/values-ms-rMY/strings.xml @@ -22,4 +22,7 @@ "Berhenti" "Gulung semula" "Mara laju" + "Ulang semua" + "Tiada ulangan" + "Ulangan" diff --git a/library/ui/src/main/res/values-my-rMM/strings.xml b/library/ui/src/main/res/values-my-rMM/strings.xml index 4b68e6e950..b4ea5b1155 100644 --- a/library/ui/src/main/res/values-my-rMM/strings.xml +++ b/library/ui/src/main/res/values-my-rMM/strings.xml @@ -22,4 +22,7 @@ "ရပ်ရန်" "ပြန်ရစ်ရန်" "ရှေ့သို့ သွားရန်" + "အားလုံး ထပ်တလဲလဲဖွင့်ရန်" + "ထပ်တလဲလဲမဖွင့်ရန်" + "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်" diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index 37454235ad..679bf1134c 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -22,4 +22,7 @@ "Stopp" "Tilbakespoling" "Fremoverspoling" + "Gjenta alle" + "Ikke gjenta noen" + "Gjenta én" diff --git a/library/ui/src/main/res/values-ne-rNP/strings.xml b/library/ui/src/main/res/values-ne-rNP/strings.xml index 375e44afce..43730c1880 100644 --- a/library/ui/src/main/res/values-ne-rNP/strings.xml +++ b/library/ui/src/main/res/values-ne-rNP/strings.xml @@ -22,4 +22,7 @@ "रोक्नुहोस्" "दोहोर्याउनुहोस्" "फास्ट फर्वार्ड" + "सबै दोहोर्याउनुहोस्" + "कुनै पनि नदोहोर्याउनुहोस्" + "एउटा दोहोर्याउनुहोस्" diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index 2bdbf0bdae..6383c977fc 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -22,4 +22,7 @@ "Stoppen" "Terugspoelen" "Vooruitspoelen" + "Alles herhalen" + "Niet herhalen" + "Eén herhalen" diff --git a/library/ui/src/main/res/values-pa-rIN/strings.xml b/library/ui/src/main/res/values-pa-rIN/strings.xml index 143508e071..ddf60b0394 100644 --- a/library/ui/src/main/res/values-pa-rIN/strings.xml +++ b/library/ui/src/main/res/values-pa-rIN/strings.xml @@ -22,4 +22,7 @@ "ਰੋਕੋ" "ਰੀਵਾਈਂਡ ਕਰੋ" "ਅੱਗੇ ਭੇਜੋ" + "ਸਭ ਨੂੰ ਦੁਹਰਾਓ" + "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ" + "ਇੱਕ ਦੁਹਰਾਓ" diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 64f52d5d09..113c568f85 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -22,4 +22,7 @@ "Zatrzymaj" "Przewiń do tyłu" "Przewiń do przodu" + "Powtórz wszystkie" + "Nie powtarzaj" + "Powtórz jeden" diff --git a/library/ui/src/main/res/values-pt-rBR/strings.xml b/library/ui/src/main/res/values-pt-rBR/strings.xml index 51bcf4d723..87c54358ba 100644 --- a/library/ui/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui/src/main/res/values-pt-rBR/strings.xml @@ -22,4 +22,7 @@ "Parar" "Retroceder" "Avançar" + "Repetir tudo" + "Não repetir" + "Repetir um" diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml index 5b3c9131d0..ca34afec3c 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -22,4 +22,7 @@ "Parar" "Rebobinar" "Avançar" + "Repetir tudo" + "Não repetir" + "Repetir um" diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 51bcf4d723..2fc3191738 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -22,4 +22,7 @@ "Parar" "Retroceder" "Avançar" + "Repetir tudo" + "Não repetir" + "Repetir uma" diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 5a7feda78c..0b2ce540f7 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -22,4 +22,7 @@ "Opriți" "Derulați" "Derulați rapid înainte" + "Repetați toate" + "Repetați niciuna" + "Repetați unul" diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index da47546a8b..1d179e028c 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -22,4 +22,7 @@ "Остановить" "Перемотать назад" "Перемотать вперед" + "Повторять все" + "Не повторять" + "Повторять один элемент" diff --git a/library/ui/src/main/res/values-si-rLK/strings.xml b/library/ui/src/main/res/values-si-rLK/strings.xml index 0b579240e8..bc37d98eed 100644 --- a/library/ui/src/main/res/values-si-rLK/strings.xml +++ b/library/ui/src/main/res/values-si-rLK/strings.xml @@ -22,4 +22,7 @@ "නතර කරන්න" "නැවත ඔතන්න" "වේගයෙන් ඉදිරියට යන" + "සියලු නැවත" + "කිසිවක් නැවත" + "නැවත නැවත එක්" diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index 7596497e06..a6ea26bdf0 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -22,4 +22,7 @@ "Zastaviť" "Pretočiť späť" "Pretočiť dopredu" + "Opakovať všetko" + "Neopakovať" + "Opakovať jednu položku" diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index a77586b50c..39813fa385 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -22,4 +22,7 @@ "Ustavi" "Previj nazaj" "Previj naprej" + "Ponovi vse" + "Ne ponovi" + "Ponovi eno" diff --git a/library/ui/src/main/res/values-sq-rAL/strings.xml b/library/ui/src/main/res/values-sq-rAL/strings.xml index 1fb824366d..0bdc2e5f84 100644 --- a/library/ui/src/main/res/values-sq-rAL/strings.xml +++ b/library/ui/src/main/res/values-sq-rAL/strings.xml @@ -22,4 +22,7 @@ "Ndalo" "Kthehu pas" "Përparo me shpejtësi" + "Përsërit të gjithë" + "Përsëritni asnjë" + "Përsëritni një" diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index 175ad4fe7f..0d54de5f6a 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -22,4 +22,7 @@ "Заустави" "Премотај уназад" "Премотај унапред" + "Понови све" + "Понављање је искључено" + "Понови једну" diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index e6a8960458..0f7f16f91d 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -22,4 +22,7 @@ "Avbryt" "Spola tillbaka" "Snabbspola framåt" + "Upprepa alla" + "Upprepa inga" + "Upprepa en" diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index 8055b7daff..b48af88659 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -22,4 +22,7 @@ "Simamisha" "Rudisha nyuma" "Peleka mbele kwa kasi" + "Rudia zote" + "Usirudie Yoyote" + "Rudia Moja" diff --git a/library/ui/src/main/res/values-ta-rIN/strings.xml b/library/ui/src/main/res/values-ta-rIN/strings.xml index 3eb995d467..3dd64f52f7 100644 --- a/library/ui/src/main/res/values-ta-rIN/strings.xml +++ b/library/ui/src/main/res/values-ta-rIN/strings.xml @@ -22,4 +22,7 @@ "நிறுத்து" "மீண்டும் காட்டு" "வேகமாக முன்செல்" + "அனைத்தையும் மீண்டும் இயக்கு" + "எதையும் மீண்டும் இயக்காதே" + "ஒன்றை மட்டும் மீண்டும் இயக்கு" diff --git a/library/ui/src/main/res/values-te-rIN/strings.xml b/library/ui/src/main/res/values-te-rIN/strings.xml index fe7930455a..daf337a931 100644 --- a/library/ui/src/main/res/values-te-rIN/strings.xml +++ b/library/ui/src/main/res/values-te-rIN/strings.xml @@ -22,4 +22,7 @@ "ఆపివేయి" "రివైండ్ చేయి" "వేగంగా ఫార్వార్డ్ చేయి" + "అన్నీ పునరావృతం చేయి" + "ఏదీ పునరావృతం చేయవద్దు" + "ఒకదాన్ని పునరావృతం చేయి" diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index deb2aac87d..ff89b8d5f5 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -22,4 +22,7 @@ "หยุด" "กรอกลับ" "กรอไปข้างหน้า" + "เล่นซ้ำทั้งหมด" + "ไม่เล่นซ้ำ" + "เล่นซ้ำรายการเดียว" diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 28dcb3267e..89cf2ef400 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -22,4 +22,7 @@ "Ihinto" "I-rewind" "I-fast forward" + "Ulitin Lahat" + "Walang Uulitin" + "Ulitin ang Isa" diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index 4265d796fe..87dba7204c 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -22,4 +22,7 @@ "Durdur" "Geri sar" "İleri sar" + "Tümünü Tekrarla" + "Hiçbirini Tekrarlama" + "Birini Tekrarla" diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 487ca07556..1fdfe2bce5 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -22,4 +22,7 @@ "Зупинити" "Перемотати назад" "Перемотати вперед" + "Повторити все" + "Не повторювати" + "Повторити один елемент" diff --git a/library/ui/src/main/res/values-ur-rPK/strings.xml b/library/ui/src/main/res/values-ur-rPK/strings.xml index 55fa908bcd..956374b26a 100644 --- a/library/ui/src/main/res/values-ur-rPK/strings.xml +++ b/library/ui/src/main/res/values-ur-rPK/strings.xml @@ -22,4 +22,7 @@ "روکیں" "ریوائینڈ کریں" "تیزی سے فارورڈ کریں" + "سبھی کو دہرائیں" + "کسی کو نہ دہرائیں" + "ایک کو دہرائیں" diff --git a/library/ui/src/main/res/values-uz-rUZ/strings.xml b/library/ui/src/main/res/values-uz-rUZ/strings.xml index 9cee926844..286d4d01ab 100644 --- a/library/ui/src/main/res/values-uz-rUZ/strings.xml +++ b/library/ui/src/main/res/values-uz-rUZ/strings.xml @@ -22,4 +22,7 @@ "To‘xtatish" "Orqaga o‘tkazish" "Oldinga o‘tkazish" + "Barchasini takrorlash" + "Takrorlamaslik" + "Bir marta takrorlash" diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 917ec8e95c..4dea58d494 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -22,4 +22,7 @@ "Ngừng" "Tua lại" "Tua đi" + "Lặp lại tất cả" + "Không lặp lại" + "Lặp lại một mục" diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml index 41e02409e2..e15d84e777 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -22,4 +22,7 @@ "停止" "快退" "快进" + "重复播放全部" + "不重复播放" + "重复播放单个视频" diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml index a3244bcd70..ba793e98a8 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -22,4 +22,7 @@ "停止" "倒帶" "向前快轉" + "重複播放所有媒體項目" + "不重複播放任何媒體項目" + "重複播放一個媒體項目" diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml index ee915c5d9d..bf3364d5cf 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -22,4 +22,7 @@ "停止" "倒轉" "快轉" + "重複播放所有媒體項目" + "不重複播放" + "重複播放單一媒體項目" diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index e998846454..d7bebaaa2a 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -22,4 +22,7 @@ "Misa" "Buyisela emumva" "Ukudlulisa ngokushesha" + "Phinda konke" + "Ungaphindi lutho" + "Phida okukodwa" diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index d8340c21cd..d1f45228b1 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -34,12 +34,18 @@ + + + + + + @@ -57,6 +63,7 @@ + @@ -72,6 +79,7 @@ + diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index 61db83825e..b16b1729da 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -20,6 +20,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index 1e652dddb3..c5d11eeadb 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -21,4 +21,7 @@ Stop Rewind Fast forward + Repeat none + Repeat one + Repeat all diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 6a09eac49e..6cd56868f9 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +apply from: '../constants.gradle' apply plugin: 'com.android.library' android { @@ -24,7 +25,8 @@ android { } dependencies { - compile project(':library-core') - androidTestCompile project(':library-dash') - androidTestCompile project(':library-hls') + androidTestCompile project(modulePrefix + 'library-core') + androidTestCompile project(modulePrefix + 'library-dash') + androidTestCompile project(modulePrefix + 'library-hls') + androidTestCompile project(modulePrefix + 'testutils') } diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index 2f7bbe6d7c..053fe4e61c 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -28,7 +28,7 @@ tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> - @@ -36,7 +36,6 @@ + android:name="android.test.InstrumentationTestRunner"/> diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java new file mode 100644 index 0000000000..3f84b9ea85 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.playbacktests.gts; + +import android.test.ActivityInstrumentationTestCase2; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.HostActivity; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; + +/** + * Test playback of encrypted DASH streams using different CENC scheme types. + */ +public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "CencDrmTest"; + + private static final String URL_cenc = + "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"; + private static final String URL_cbc1 = + "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"; + private static final String URL_cbcs = + "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"; + private static final String ID_AUDIO = "0"; + private static final String[] IDS_VIDEO = new String[] {"1", "2"}; + + // Seeks help reproduce playback issues in certain devices. + private static final ActionSchedule ACTION_SCHEDULE_WITH_SEEKS = new ActionSchedule.Builder(TAG) + .delay(30000).seek(300000).delay(10000).seek(270000).delay(10000).seek(200000).delay(10000) + .stop().build(); + + private DashTestRunner testRunner; + + public CommonEncryptionDrmTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) + .setWidevineInfo(MimeTypes.VIDEO_H264, false) + .setActionSchedule(ACTION_SCHEDULE_WITH_SEEKS) + .setAudioVideoFormats(ID_AUDIO, IDS_VIDEO) + .setCanIncludeAdditionalVideoFormats(true); + } + + @Override + protected void tearDown() throws Exception { + testRunner = null; + super.tearDown(); + } + + public void testCencSchemeType() { + if (Util.SDK_INT < 18) { + // Pass. + return; + } + testRunner.setStreamName("test_widevine_h264_scheme_cenc").setManifestUrl(URL_cenc).run(); + } + + public void testCbc1SchemeType() { + if (Util.SDK_INT < 24) { + // Pass. + return; + } + testRunner.setStreamName("test_widevine_h264_scheme_cbc1").setManifestUrl(URL_cbc1).run(); + } + + public void testCbcsSchemeType() { + if (Util.SDK_INT < 24) { + // Pass. + return; + } + testRunner.setStreamName("test_widevine_h264_scheme_cbcs").setManifestUrl(URL_cbcs).run(); + } + + public void testCensSchemeType() { + // TODO: Implement once content is available. Track [internal: b/31219813]. + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index e7441362cf..529f57582e 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -20,8 +20,8 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; -import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -67,6 +67,10 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 - + diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java similarity index 63% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/Action.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 64484f7c5d..b1c6f081cf 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.util.Log; +import android.view.Surface; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -41,19 +43,24 @@ public abstract class Action { * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. + * @param surface The surface to use when applying actions. */ - public final void doAction(ExoPlayer player, MappingTrackSelector trackSelector) { + public final void doAction(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { Log.i(tag, description); - doActionImpl(player, trackSelector); + doActionImpl(player, trackSelector, surface); } /** - * Called by {@link #doAction(ExoPlayer, MappingTrackSelector)} do actually perform the action. + * Called by {@link #doAction(SimpleExoPlayer, MappingTrackSelector, Surface)} do perform the + * action. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. + * @param surface The surface to use when applying actions. */ - protected abstract void doActionImpl(ExoPlayer player, MappingTrackSelector trackSelector); + protected abstract void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface); /** * Calls {@link ExoPlayer#seekTo(long)}. @@ -72,7 +79,8 @@ public abstract class Action { } @Override - protected void doActionImpl(ExoPlayer player, MappingTrackSelector trackSelector) { + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { player.seekTo(positionMs); } @@ -91,7 +99,8 @@ public abstract class Action { } @Override - protected void doActionImpl(ExoPlayer player, MappingTrackSelector trackSelector) { + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { player.stop(); } @@ -114,7 +123,8 @@ public abstract class Action { } @Override - protected void doActionImpl(ExoPlayer player, MappingTrackSelector trackSelector) { + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { player.setPlayWhenReady(playWhenReady); } @@ -140,10 +150,52 @@ public abstract class Action { } @Override - protected void doActionImpl(ExoPlayer player, MappingTrackSelector trackSelector) { + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { trackSelector.setRendererDisabled(rendererIndex, disabled); } } + /** + * Calls {@link SimpleExoPlayer#clearVideoSurface()}. + */ + public static final class ClearVideoSurface extends Action { + + /** + * @param tag A tag to use for logging. + */ + public ClearVideoSurface(String tag) { + super(tag, "ClearVideoSurface"); + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.clearVideoSurface(); + } + + } + + /** + * Calls {@link SimpleExoPlayer#setVideoSurface(Surface)}. + */ + public static final class SetVideoSurface extends Action { + + /** + * @param tag A tag to use for logging. + */ + public SetVideoSurface(String tag) { + super(tag, "SetVideoSurface"); + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setVideoSurface(surface); + } + + } + + } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java similarity index 63% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ActionSchedule.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 5e2ae24c2c..66f7ebca95 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -13,14 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.os.Handler; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.playbacktests.util.Action.Seek; -import com.google.android.exoplayer2.playbacktests.util.Action.SetPlayWhenReady; -import com.google.android.exoplayer2.playbacktests.util.Action.SetRendererDisabled; -import com.google.android.exoplayer2.playbacktests.util.Action.Stop; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; +import com.google.android.exoplayer2.testutil.Action.Seek; +import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; +import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; +import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; +import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -42,11 +46,12 @@ public final class ActionSchedule { * * @param player The player to which actions should be applied. * @param trackSelector The track selector to which actions should be applied. + * @param surface The surface to use when applying actions. * @param mainHandler A handler associated with the main thread of the host activity. */ - /* package */ void start(ExoPlayer player, MappingTrackSelector trackSelector, - Handler mainHandler) { - rootNode.schedule(player, trackSelector, mainHandler); + /* package */ void start(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface, Handler mainHandler) { + rootNode.schedule(player, trackSelector, surface, mainHandler); } /** @@ -87,11 +92,18 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder apply(Action action) { - ActionNode next = new ActionNode(action, currentDelayMs); - previousNode.setNext(next); - previousNode = next; - currentDelayMs = 0; - return this; + return appendActionNode(new ActionNode(action, currentDelayMs)); + } + + /** + * Schedules an action to be executed repeatedly. + * + * @param action The action to schedule. + * @param intervalMs The interval between each repetition in milliseconds. + * @return The builder, for convenience. + */ + public Builder repeat(Action action, long intervalMs) { + return appendActionNode(new ActionNode(action, currentDelayMs, intervalMs)); } /** @@ -149,10 +161,35 @@ public final class ActionSchedule { return apply(new SetRendererDisabled(tag, index, true)); } + /** + * Schedules a clear video surface action to be executed. + * + * @return The builder, for convenience. + */ + public Builder clearVideoSurface() { + return apply(new ClearVideoSurface(tag)); + } + + /** + * Schedules a set video surface action to be executed. + * + * @return The builder, for convenience. + */ + public Builder setVideoSurface() { + return apply(new SetVideoSurface(tag)); + } + public ActionSchedule build() { return new ActionSchedule(rootNode); } + private Builder appendActionNode(ActionNode actionNode) { + previousNode.setNext(actionNode); + previousNode = actionNode; + currentDelayMs = 0; + return this; + } + } /** @@ -162,11 +199,13 @@ public final class ActionSchedule { private final Action action; private final long delayMs; + private final long repeatIntervalMs; private ActionNode next; - private ExoPlayer player; + private SimpleExoPlayer player; private MappingTrackSelector trackSelector; + private Surface surface; private Handler mainHandler; /** @@ -174,8 +213,19 @@ public final class ActionSchedule { * @param delayMs The delay between the node being scheduled and the action being executed. */ public ActionNode(Action action, long delayMs) { + this(action, delayMs, C.TIME_UNSET); + } + + /** + * @param action The wrapped action. + * @param delayMs The delay between the node being scheduled and the action being executed. + * @param repeatIntervalMs The interval between one execution and the next repetition. If set to + * {@link C#TIME_UNSET}, the action is executed once only. + */ + public ActionNode(Action action, long delayMs, long repeatIntervalMs) { this.action = action; this.delayMs = delayMs; + this.repeatIntervalMs = repeatIntervalMs; } /** @@ -193,21 +243,26 @@ public final class ActionSchedule { * * @param player The player to which actions should be applied. * @param trackSelector The track selector to which actions should be applied. + * @param surface The surface to use when applying actions. * @param mainHandler A handler associated with the main thread of the host activity. */ - public void schedule(ExoPlayer player, MappingTrackSelector trackSelector, - Handler mainHandler) { + public void schedule(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface, Handler mainHandler) { this.player = player; this.trackSelector = trackSelector; + this.surface = surface; this.mainHandler = mainHandler; mainHandler.postDelayed(this, delayMs); } @Override public void run() { - action.doAction(player, trackSelector); + action.doAction(player, trackSelector, surface); if (next != null) { - next.schedule(player, trackSelector, mainHandler); + next.schedule(player, trackSelector, surface, mainHandler); + } + if (repeatIntervalMs != C.TIME_UNSET) { + mainHandler.postDelayed(this, repeatIntervalMs); } } @@ -223,7 +278,8 @@ public final class ActionSchedule { } @Override - protected void doActionImpl(ExoPlayer player, MappingTrackSelector trackSelector) { + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { // Do nothing. } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java new file mode 100644 index 0000000000..c8ead5dcba --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -0,0 +1,109 @@ +/* + * 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.testutil; + +import static junit.framework.Assert.assertEquals; + +import android.net.Uri; +import android.test.MoreAsserts; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheUtil; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import junit.framework.Assert; + +/** + * Assertion methods for {@link Cache}. + */ +public final class CacheAsserts { + + /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + ArrayList allData = fakeDataSet.getAllData(); + String[] uriStrings = new String[allData.size()]; + for (int i = 0; i < allData.size(); i++) { + uriStrings[i] = allData.get(i).uri; + } + assertCachedData(cache, fakeDataSet, uriStrings); + } + + /** + * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) + throws IOException { + int totalLength = 0; + for (String uriString : uriStrings) { + byte[] data = fakeDataSet.getData(uriString).getData(); + assertDataCached(cache, uriString, data); + totalLength += data.length; + } + assertEquals(totalLength, cache.getCacheSpace()); + } + + /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) + throws IOException { + for (String uriString : uriStrings) { + assertDataCached(cache, uriString, fakeDataSet.getData(uriString).getData()); + } + } + + /** Asserts that the cache contains the given data for {@code uriString}. */ + public static void assertDataCached(Cache cache, String uriString, byte[] expected) + throws IOException { + CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(Uri.parse(uriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + try { + inputStream.open(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + // Ignore + } finally { + inputStream.close(); + } + MoreAsserts.assertEquals("Cached data doesn't match expected for '" + uriString + "',", + expected, outputStream.toByteArray()); + } + + /** Asserts that there is no cache content for the given {@code uriStrings}. */ + public static void assertDataNotCached(Cache cache, String... uriStrings) { + for (String uriString : uriStrings) { + Assert.assertNull("There is cached data for '" + uriString + "',", + cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))); + } + } + + /** Asserts that the cache is empty. */ + public static void assertCacheEmpty(Cache cache) { + assertEquals(0, cache.getCacheSpace()); + } + + private CacheAsserts() {} + +} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java similarity index 98% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 6cb7673ebd..af7c1a3e2a 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.annotation.TargetApi; import android.content.Context; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DecoderCountersUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java similarity index 97% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DecoderCountersUtil.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java index aafb828345..448ec79c2d 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/DecoderCountersUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.decoder.DecoderCounters; import junit.framework.TestCase; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java similarity index 90% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index f48318687d..b61b484e32 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.SystemClock; @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -33,9 +34,9 @@ import com.google.android.exoplayer2.audio.AudioTrack; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; @@ -51,7 +52,7 @@ import junit.framework.Assert; /** * A {@link HostedTest} for {@link ExoPlayer} playback tests. */ -public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListener, +public abstract class ExoHostedTest implements HostedTest, Player.EventListener, AudioRendererEventListener, VideoRendererEventListener { static { @@ -76,7 +77,9 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen private Handler actionHandler; private MappingTrackSelector trackSelector; private SimpleExoPlayer player; + private Surface surface; private ExoPlaybackException playerError; + private Player.EventListener playerEventListener; private boolean playerWasPrepared; private boolean playerFinished; private boolean playing; @@ -124,7 +127,17 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen if (player == null) { pendingSchedule = schedule; } else { - schedule.start(player, trackSelector, actionHandler); + schedule.start(player, trackSelector, surface, actionHandler); + } + } + + /** + * Sets an {@link Player.EventListener} to listen for ExoPlayer events during the test. + */ + public final void setEventListener(Player.EventListener eventListener) { + this.playerEventListener = eventListener; + if (player != null) { + player.addListener(eventListener); } } @@ -132,6 +145,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen @Override public final void onStart(HostActivity host, Surface surface) { + this.surface = surface; // Build the player. DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); trackSelector = buildTrackSelector(host, bandwidthMeter); @@ -139,6 +153,9 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); player = buildExoPlayer(host, surface, trackSelector, drmSessionManager); player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), bandwidthMeter)); + if (playerEventListener != null) { + player.addListener(playerEventListener); + } player.addListener(this); player.setAudioDebugListener(this); player.setVideoDebugListener(this); @@ -146,7 +163,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen actionHandler = new Handler(); // Schedule any pending actions. if (pendingSchedule != null) { - pendingSchedule.start(player, trackSelector, actionHandler); + pendingSchedule.start(player, trackSelector, surface, actionHandler); pendingSchedule = null; } } @@ -184,7 +201,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen assertPassed(audioDecoderCounters, videoDecoderCounters); } - // ExoPlayer.EventListener + // Player.EventListener @Override public void onLoadingChanged(boolean isLoading) { @@ -199,12 +216,12 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen @Override public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]"); - playerWasPrepared |= playbackState != ExoPlayer.STATE_IDLE; - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playerWasPrepared)) { + playerWasPrepared |= playbackState != Player.STATE_IDLE; + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { playerFinished = true; } - boolean playing = playWhenReady && playbackState == ExoPlayer.STATE_READY; + boolean playing = playWhenReady && playbackState == Player.STATE_READY; if (!this.playing && playing) { lastPlayingStartTimeMs = SystemClock.elapsedRealtime(); } else if (this.playing && !playing) { @@ -213,6 +230,11 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen this.playing = playing; } + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + @Override public final void onPlayerError(ExoPlaybackException error) { playerWasPrepared = true; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java new file mode 100644 index 0000000000..ab247283e6 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java @@ -0,0 +1,192 @@ +/* + * 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.testutil; + +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Pair; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.util.LinkedList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import junit.framework.Assert; + +/** + * Wraps a player with its own handler thread. + */ +public class ExoPlayerWrapper implements Player.EventListener { + + private final CountDownLatch sourceInfoCountDownLatch; + private final CountDownLatch endedCountDownLatch; + private final HandlerThread playerThread; + private final Handler handler; + private final LinkedList> sourceInfos; + + public ExoPlayer player; + public TrackGroupArray trackGroups; + public Exception exception; + + // Written only on the main thread. + public volatile int positionDiscontinuityCount; + + public ExoPlayerWrapper() { + sourceInfoCountDownLatch = new CountDownLatch(1); + endedCountDownLatch = new CountDownLatch(1); + playerThread = new HandlerThread("ExoPlayerTest thread"); + playerThread.start(); + handler = new Handler(playerThread.getLooper()); + sourceInfos = new LinkedList<>(); + } + + // Called on the test thread. + + public void blockUntilEnded(long timeoutMs) throws Exception { + if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + exception = new TimeoutException("Test playback timed out waiting for playback to end."); + } + release(); + // Throw any pending exception (from playback, timing out or releasing). + if (exception != null) { + throw exception; + } + } + + public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception { + if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Test playback timed out waiting for source info."); + } + } + + public void setup(final MediaSource mediaSource, final Renderer... renderers) { + handler.post(new Runnable() { + @Override + public void run() { + try { + player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); + player.addListener(ExoPlayerWrapper.this); + player.setPlayWhenReady(true); + player.prepare(mediaSource); + } catch (Exception e) { + handleError(e); + } + } + }); + } + + 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(); + } + + @SafeVarargs + public final void assertSourceInfosEquals(Pair... sourceInfos) { + Assert.assertEquals(sourceInfos.length, this.sourceInfos.size()); + for (Pair sourceInfo : sourceInfos) { + Assert.assertEquals(sourceInfo, this.sourceInfos.remove()); + } + } + + // Player.EventListener implementation. + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_ENDED) { + endedCountDownLatch.countDown(); + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + + @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. + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java new file mode 100644 index 0000000000..db63662c45 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -0,0 +1,289 @@ +/* + * 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.testutil; + +import android.app.Instrumentation; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Arrays; +import junit.framework.Assert; + +/** + * Assertion methods for {@link Extractor}. + */ +public final class ExtractorAsserts { + + /** + * A factory for {@link Extractor} instances. + */ + public interface ExtractorFactory { + Extractor create(); + } + + private static final String DUMP_EXTENSION = ".dump"; + private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; + + /** + * Asserts that an extractor behaves correctly given valid input data: + *

        + *
      • Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling + * {@link Extractor#init(ExtractorOutput)} to check these calls do not fail.
      • + *
      • Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, + * boolean, boolean)} with all possible combinations of "simulate" parameters.
      • + *
      + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param file The path to the input sample. + * @param instrumentation To be used to load the sample file. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertBehavior(ExtractorFactory factory, String file, + Instrumentation instrumentation) throws IOException, InterruptedException { + // Check behavior prior to initialization. + Extractor extractor = factory.create(); + extractor.seek(0, 0); + extractor.release(); + // Assert output. + byte[] fileData = TestUtil.getByteArray(instrumentation, file); + assertOutput(factory, file, fileData, instrumentation); + } + + /** + * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, + * boolean, boolean)} with all possible combinations of "simulate" parameters with + * {@code sniffFirst} set to true, and makes one additional call with the "simulate" and + * {@code sniffFirst} parameters all set to false. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param file The path to the input sample. + * @param data Content of the input file. + * @param instrumentation To be used to load the sample file. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertOutput(ExtractorFactory factory, String file, byte[] data, + Instrumentation instrumentation) throws IOException, InterruptedException { + assertOutput(factory.create(), file, data, instrumentation, true, false, false, false); + assertOutput(factory.create(), file, data, instrumentation, true, false, false, true); + assertOutput(factory.create(), file, data, instrumentation, true, false, true, false); + assertOutput(factory.create(), file, data, instrumentation, true, false, true, true); + assertOutput(factory.create(), file, data, instrumentation, true, true, false, false); + assertOutput(factory.create(), file, data, instrumentation, true, true, false, true); + assertOutput(factory.create(), file, data, instrumentation, true, true, true, false); + assertOutput(factory.create(), file, data, instrumentation, true, true, true, true); + assertOutput(factory.create(), file, data, instrumentation, false, false, false, false); + } + + /** + * Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals + * to a prerecorded output dump file with the name {@code sampleFile} + "{@value + * #DUMP_EXTENSION}". If {@code simulateUnknownLength} is true and {@code sampleFile} + "{@value + * #UNKNOWN_LENGTH_EXTENSION}" exists, it's preferred. + * + * @param extractor The {@link Extractor} to be tested. + * @param file The path to the input sample. + * @param data Content of the input file. + * @param instrumentation To be used to load the sample file. + * @param sniffFirst Whether to sniff the data by calling {@link Extractor#sniff(ExtractorInput)} + * prior to consuming it. + * @param simulateIOErrors Whether to simulate IO errors. + * @param simulateUnknownLength Whether to simulate unknown input length. + * @param simulatePartialReads Whether to simulate partial reads. + * @return The {@link FakeExtractorOutput} used in the test. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static FakeExtractorOutput assertOutput(Extractor extractor, String file, byte[] data, + Instrumentation instrumentation, boolean sniffFirst, boolean simulateIOErrors, + boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, + InterruptedException { + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data) + .setSimulateIOErrors(simulateIOErrors) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(simulatePartialReads).build(); + + if (sniffFirst) { + Assert.assertTrue(TestUtil.sniffTestData(extractor, input)); + input.resetPeekPosition(); + } + + FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); + if (simulateUnknownLength + && assetExists(instrumentation, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(instrumentation, file + UNKNOWN_LENGTH_EXTENSION); + } else { + extractorOutput.assertOutput(instrumentation, file + ".0" + DUMP_EXTENSION); + } + + SeekMap seekMap = extractorOutput.seekMap; + if (seekMap.isSeekable()) { + long durationUs = seekMap.getDurationUs(); + for (int j = 0; j < 4; j++) { + long timeUs = (durationUs * j) / 3; + long position = seekMap.getPosition(timeUs); + input.setPosition((int) position); + for (int i = 0; i < extractorOutput.numberOfTracks; i++) { + extractorOutput.trackOutputs.valueAt(i).clear(); + } + + consumeTestData(extractor, input, timeUs, extractorOutput, false); + extractorOutput.assertOutput(instrumentation, file + '.' + j + DUMP_EXTENSION); + } + } + + return extractorOutput; + } + + /** + * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all + * possible combinations of "simulate" parameters. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param sampleFile The path to the input sample. + * @param instrumentation To be used to load the sample file. + * @param expectedThrowable Expected {@link Throwable} class. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) + */ + public static void assertThrows(ExtractorFactory factory, String sampleFile, + Instrumentation instrumentation, Class expectedThrowable) + throws IOException, InterruptedException { + byte[] fileData = TestUtil.getByteArray(instrumentation, sampleFile); + assertThrows(factory, fileData, expectedThrowable); + } + + /** + * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all + * possible combinations of "simulate" parameters. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param fileData Content of the input file. + * @param expectedThrowable Expected {@link Throwable} class. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) + */ + public static void assertThrows(ExtractorFactory factory, byte[] fileData, + Class expectedThrowable) throws IOException, InterruptedException { + assertThrows(factory.create(), fileData, expectedThrowable, false, false, false); + assertThrows(factory.create(), fileData, expectedThrowable, true, false, false); + assertThrows(factory.create(), fileData, expectedThrowable, false, true, false); + assertThrows(factory.create(), fileData, expectedThrowable, true, true, false); + assertThrows(factory.create(), fileData, expectedThrowable, false, false, true); + assertThrows(factory.create(), fileData, expectedThrowable, true, false, true); + assertThrows(factory.create(), fileData, expectedThrowable, false, true, true); + assertThrows(factory.create(), fileData, expectedThrowable, true, true, true); + } + + /** + * Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code sampleFile}. + * + * @param extractor The {@link Extractor} to be tested. + * @param fileData Content of the input file. + * @param expectedThrowable Expected {@link Throwable} class. + * @param simulateIOErrors If true simulates IOErrors. + * @param simulateUnknownLength If true simulates unknown input length. + * @param simulatePartialReads If true simulates partial reads. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertThrows(Extractor extractor, byte[] fileData, + Class expectedThrowable, boolean simulateIOErrors, + boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, + InterruptedException { + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) + .setSimulateIOErrors(simulateIOErrors) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(simulatePartialReads).build(); + try { + consumeTestData(extractor, input, 0, true); + throw new AssertionError(expectedThrowable.getSimpleName() + " expected but not thrown"); + } catch (Throwable throwable) { + if (expectedThrowable.equals(throwable.getClass())) { + return; // Pass! + } + throw throwable; + } + } + + private ExtractorAsserts() {} + + private static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input, + long timeUs, boolean retryFromStartIfLive) throws IOException, InterruptedException { + FakeExtractorOutput output = new FakeExtractorOutput(); + extractor.init(output); + consumeTestData(extractor, input, timeUs, output, retryFromStartIfLive); + return output; + } + + private static void consumeTestData(Extractor extractor, FakeExtractorInput input, long timeUs, + FakeExtractorOutput output, boolean retryFromStartIfLive) + throws IOException, InterruptedException { + extractor.seek(input.getPosition(), timeUs); + PositionHolder seekPositionHolder = new PositionHolder(); + int readResult = Extractor.RESULT_CONTINUE; + while (readResult != Extractor.RESULT_END_OF_INPUT) { + try { + // Extractor.read should not read seekPositionHolder.position. Set it to a value that's + // likely to cause test failure if a read does occur. + seekPositionHolder.position = Long.MIN_VALUE; + readResult = extractor.read(input, seekPositionHolder); + if (readResult == Extractor.RESULT_SEEK) { + long seekPosition = seekPositionHolder.position; + Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE); + input.setPosition((int) seekPosition); + } + } catch (SimulatedIOException e) { + if (!retryFromStartIfLive) { + continue; + } + boolean isOnDemand = input.getLength() != C.LENGTH_UNSET + || (output.seekMap != null && output.seekMap.getDurationUs() != C.TIME_UNSET); + if (isOnDemand) { + continue; + } + input.setPosition(0); + for (int i = 0; i < output.numberOfTracks; i++) { + output.trackOutputs.valueAt(i).clear(); + } + extractor.seek(0, 0); + } + } + } + + private static boolean assetExists(Instrumentation instrumentation, String fileName) + throws IOException { + int i = fileName.lastIndexOf('/'); + String path = i >= 0 ? fileName.substring(0, i) : ""; + String file = i >= 0 ? fileName.substring(i + 1) : fileName; + return Arrays.asList(instrumentation.getContext().getResources().getAssets().list(path)) + .contains(file); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java new file mode 100644 index 0000000000..f4476ddf93 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java @@ -0,0 +1,93 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.trackselection.TrackSelection; + +/** + * Fake data set emulating the data of an adaptive media source. + * It provides chunk data for all {@link Format}s in the given {@link TrackSelection}. + */ +public final class FakeAdaptiveDataSet extends FakeDataSet { + + /** + * Factory for {@link FakeAdaptiveDataSet}s. + */ + public static final class Factory { + + private final long chunkDurationUs; + + public Factory(long chunkDurationUs) { + this.chunkDurationUs = chunkDurationUs; + } + + public FakeAdaptiveDataSet createDataSet(TrackSelection trackSelection, long mediaDurationUs) { + return new FakeAdaptiveDataSet(trackSelection, mediaDurationUs, chunkDurationUs); + } + + } + + private final long chunkCount; + private final long chunkDurationUs; + private final long lastChunkDurationUs; + + public FakeAdaptiveDataSet(TrackSelection trackSelection, long mediaDurationUs, + long chunkDurationUs) { + this.chunkDurationUs = chunkDurationUs; + int selectionCount = trackSelection.length(); + long lastChunkDurationUs = mediaDurationUs % chunkDurationUs; + int fullChunks = (int) (mediaDurationUs / chunkDurationUs); + for (int i = 0; i < selectionCount; i++) { + String uri = getUri(i); + Format format = trackSelection.getFormat(i); + int chunkLength = (int) (format.bitrate * chunkDurationUs / (8 * C.MICROS_PER_SECOND)); + FakeData newData = this.newData(uri); + for (int j = 0; j < fullChunks; j++) { + newData.appendReadData(chunkLength); + } + if (lastChunkDurationUs > 0) { + int lastChunkLength = (int) (format.bitrate * (mediaDurationUs % chunkDurationUs) + / (8 * C.MICROS_PER_SECOND)); + newData.appendReadData(lastChunkLength); + } + } + this.lastChunkDurationUs = lastChunkDurationUs == 0 ? chunkDurationUs : lastChunkDurationUs; + this.chunkCount = lastChunkDurationUs == 0 ? fullChunks : fullChunks + 1; + } + + public long getChunkCount() { + return chunkCount; + } + + public String getUri(int trackSelectionIndex) { + return "fake://adaptive.media/" + Integer.toString(trackSelectionIndex); + } + + public long getChunkDuration(int chunkIndex) { + return chunkIndex == getChunkCount() - 1 ? lastChunkDurationUs : chunkDurationUs; + } + + public long getStartTime(int chunkIndex) { + return chunkIndex * chunkDurationUs; + } + + public int getChunkIndexByPosition(long positionUs) { + return (int) (positionUs / chunkDurationUs); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java new file mode 100644 index 0000000000..0c970caa15 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -0,0 +1,115 @@ +/* + * 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.testutil; + +import android.net.Uri; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.chunk.Chunk; +import com.google.android.exoplayer2.source.chunk.ChunkHolder; +import com.google.android.exoplayer2.source.chunk.ChunkSource; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.util.List; + +/** + * Fake {@link ChunkSource} with adaptive media chunks of a given duration. + */ +public final class FakeChunkSource implements ChunkSource { + + /** + * Factory for a {@link FakeChunkSource}. + */ + public static final class Factory { + + private final FakeAdaptiveDataSet.Factory dataSetFactory; + private final FakeDataSource.Factory dataSourceFactory; + + public Factory(FakeAdaptiveDataSet.Factory dataSetFactory, + FakeDataSource.Factory dataSourceFactory) { + this.dataSetFactory = dataSetFactory; + this.dataSourceFactory = dataSourceFactory; + } + + public FakeChunkSource createChunkSource(TrackSelection trackSelection, long durationUs) { + FakeAdaptiveDataSet dataSet = dataSetFactory.createDataSet(trackSelection, durationUs); + dataSourceFactory.setFakeDataSet(dataSet); + DataSource dataSource = dataSourceFactory.createDataSource(); + return new FakeChunkSource(trackSelection, dataSource, dataSet); + } + + } + + private final TrackSelection trackSelection; + private final DataSource dataSource; + private final FakeAdaptiveDataSet dataSet; + + public FakeChunkSource(TrackSelection trackSelection, DataSource dataSource, + FakeAdaptiveDataSet dataSet) { + this.trackSelection = trackSelection; + this.dataSource = dataSource; + this.dataSet = dataSet; + } + + @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override + public int getPreferredQueueSize(long playbackPositionUs, List queue) { + return trackSelection.evaluateQueueSize(playbackPositionUs, queue); + } + + @Override + public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + trackSelection.updateSelectedTrack(bufferedDurationUs); + int chunkIndex = previous == null ? dataSet.getChunkIndexByPosition(playbackPositionUs) + : previous.getNextChunkIndex(); + if (chunkIndex >= dataSet.getChunkCount()) { + out.endOfStream = true; + } else { + Format selectedFormat = trackSelection.getSelectedFormat(); + long startTimeUs = dataSet.getStartTime(chunkIndex); + long endTimeUs = startTimeUs + dataSet.getChunkDuration(chunkIndex); + String uri = dataSet.getUri(trackSelection.getSelectedIndex()); + Segment fakeDataChunk = dataSet.getData(uri).getSegments().get(chunkIndex); + DataSpec dataSpec = new DataSpec(Uri.parse(uri), fakeDataChunk.byteOffset, + fakeDataChunk.length, null); + int trackType = MimeTypes.getTrackType(selectedFormat.sampleMimeType); + out.chunk = new SingleSampleMediaChunk(dataSource, dataSpec, selectedFormat, + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, + endTimeUs, chunkIndex, trackType, selectedFormat); + } + } + + @Override + public void onChunkLoadCompleted(Chunk chunk) { + // Do nothing. + } + + @Override + public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { + return false; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java new file mode 100644 index 0000000000..36ce4b5c3e --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -0,0 +1,78 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.util.Clock; +import java.util.ArrayList; +import java.util.List; + +/** + * Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. + */ +public final class FakeClock implements Clock { + + private long currentTimeMs; + private final List wakeUpTimes; + + /** + * Create {@link FakeClock} with an arbitrary initial timestamp. + * + * @param initialTimeMs Initial timestamp in milliseconds. + */ + public FakeClock(long initialTimeMs) { + this.currentTimeMs = initialTimeMs; + this.wakeUpTimes = new ArrayList<>(); + } + + /** + * Advance timestamp of {@link FakeClock} by the specified duration. + * + * @param timeDiffMs The amount of time to add to the timestamp in milliseconds. + */ + public synchronized void advanceTime(long timeDiffMs) { + currentTimeMs += timeDiffMs; + for (Long wakeUpTime : wakeUpTimes) { + if (wakeUpTime <= currentTimeMs) { + notifyAll(); + break; + } + } + } + + @Override + public long elapsedRealtime() { + return currentTimeMs; + } + + @Override + public synchronized void sleep(long sleepTimeMs) { + if (sleepTimeMs <= 0) { + return; + } + Long wakeUpTimeMs = currentTimeMs + sleepTimeMs; + wakeUpTimes.add(wakeUpTimeMs); + while (currentTimeMs < wakeUpTimeMs) { + try { + wait(); + } catch (InterruptedException e) { + // Ignore InterruptedException as SystemClock.sleep does too. + } + } + wakeUpTimes.remove(wakeUpTimeMs); + } + +} + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java new file mode 100644 index 0000000000..2580205361 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -0,0 +1,267 @@ +/* + * 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.testutil; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Collection of {@link FakeData} to be served by a {@link FakeDataSource}. + * + *

      Multiple fake data can be defined by {@link FakeDataSet#setData(String, byte[])} and {@link + * FakeDataSet#newData(String)} methods. It's also possible to define a default data by {@link + * FakeDataSet#newDefaultData()}. + * + *

      {@link FakeDataSet#newData(String)} and {@link FakeDataSet#newDefaultData()} return a {@link + * FakeData} instance which can be used to define specific results during + * {@link FakeDataSource#read(byte[], int, int)} calls. + * + *

      The data that will be read from the source can be constructed by calling {@link + * FakeData#appendReadData(byte[])} Calls to {@link FakeDataSource#read(byte[], int, int)} will not + * span the boundaries between arrays passed to successive calls, and hence the boundaries control + * the positions at which read requests to the source may only be partially satisfied. + * + *

      Errors can be inserted by calling {@link FakeData#appendReadError(IOException)}. An inserted + * error will be thrown from the first call to {@link FakeDataSource#read(byte[], int, int)} that + * attempts to read from the corresponding position, and from all subsequent calls to + * {@link FakeDataSource#read(byte[], int, int)} until the source is closed. If the source is closed + * and re-opened having encountered an error, that error will not be thrown again. + * + *

      Actions are inserted by calling {@link FakeData#appendReadAction(Runnable)}. An actions is + * triggered when the reading reaches action's position. This can be used to make sure the code is + * in a certain state while testing. + * + *

      Example usage: + * + *

      + *   // Create a FakeDataSource then add default data and two FakeData
      + *   // "test_file" throws an IOException when tried to be read until closed and reopened.
      + *   FakeDataSource fakeDataSource = new FakeDataSource();
      + *   fakeDataSource.getDataSet()
      + *       .newDefaultData()
      + *         .appendReadData(defaultData)
      + *         .endData()
      + *       .setData("http://1", data1)
      + *       .newData("test_file")
      + *         .appendReadError(new IOException())
      + *         .appendReadData(data2)
      + *         .endData();
      + * 
      + */ +public class FakeDataSet { + + /** Container of fake data to be served by a {@link FakeDataSource}. */ + public static final class FakeData { + + /** + * A segment of {@link FakeData}. May consist of an action or exception instead of actual data. + */ + public static final class Segment { + + public @Nullable final IOException exception; + public @Nullable final byte[] data; + public final int length; + public final long byteOffset; + public @Nullable final Runnable action; + + public boolean exceptionThrown; + public boolean exceptionCleared; + public int bytesRead; + + private Segment(byte[] data, Segment previousSegment) { + this(data, data.length, null, null, previousSegment); + } + + private Segment(int length, Segment previousSegment) { + this(null, length, null, null, previousSegment); + } + + private Segment(IOException exception, Segment previousSegment) { + this(null, 0, exception, null, previousSegment); + } + + private Segment(Runnable action, Segment previousSegment) { + this(null, 0, null, action, previousSegment); + } + + private Segment(byte[] data, int length, IOException exception, Runnable action, + Segment previousSegment) { + this.exception = exception; + this.action = action; + this.data = data; + this.length = length; + this.byteOffset = previousSegment == null ? 0 + : previousSegment.byteOffset + previousSegment.length; + } + + public boolean isErrorSegment() { + return exception != null; + } + + public boolean isActionSegment() { + return action != null; + } + + } + + /** Uri of the data or null if this is the default FakeData. */ + public final String uri; + private final ArrayList segments; + private final FakeDataSet dataSet; + private boolean simulateUnknownLength; + + private FakeData(FakeDataSet dataSet, String uri) { + this.uri = uri; + this.segments = new ArrayList<>(); + this.dataSet = dataSet; + } + + /** Returns the {@link FakeDataSet} this FakeData belongs to. */ + public FakeDataSet endData() { + return dataSet; + } + + /** + * When set, {@link FakeDataSource#open(DataSpec)} will behave as though the source is unable to + * determine the length of the underlying data. Hence the return value will always be equal to + * the {@link DataSpec#length} of the argument, including the case where the length is equal to + * {@link C#LENGTH_UNSET}. + */ + public FakeData setSimulateUnknownLength(boolean simulateUnknownLength) { + this.simulateUnknownLength = simulateUnknownLength; + return this; + } + + /** + * Appends to the underlying data. + */ + public FakeData appendReadData(byte[] data) { + Assertions.checkState(data != null && data.length > 0); + segments.add(new Segment(data, getLastSegment())); + return this; + } + + /** + * Appends data of the specified length. No actual data is available and this data should not + * be read. + */ + public FakeData appendReadData(int length) { + Assertions.checkState(length > 0); + segments.add(new Segment(length, getLastSegment())); + return this; + } + + /** + * Appends an error in the underlying data. + */ + public FakeData appendReadError(IOException exception) { + segments.add(new Segment(exception, getLastSegment())); + return this; + } + + /** + * Appends an action. + */ + public FakeData appendReadAction(Runnable action) { + segments.add(new Segment(action, getLastSegment())); + return this; + } + + /** Returns the whole data added by {@link #appendReadData(byte[])}. */ + public byte[] getData() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + for (Segment segment : segments) { + if (segment.data != null) { + try { + outputStream.write(segment.data); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + } + return outputStream.toByteArray(); + } + + /** Returns the list of {@link Segment}s. */ + public List getSegments() { + return segments; + } + + /** Retuns whether unknown length is simulated */ + public boolean isSimulatingUnknownLength() { + return simulateUnknownLength; + } + + private Segment getLastSegment() { + int count = segments.size(); + return count > 0 ? segments.get(count - 1) : null; + } + + } + + private final HashMap dataMap; + private FakeData defaultData; + + public FakeDataSet() { + dataMap = new HashMap<>(); + } + + /** Sets the default data, overwrites if there is one already. */ + public FakeData newDefaultData() { + defaultData = new FakeData(this, null); + return defaultData; + } + + /** Sets random data with the given {@code length} for the given {@code uri}. */ + public FakeDataSet setRandomData(String uri, int length) { + return setData(uri, TestUtil.buildTestData(length)); + } + + /** Sets the given {@code data} for the given {@code uri}. */ + public FakeDataSet setData(String uri, byte[] data) { + return newData(uri).appendReadData(data).endData(); + } + + /** Returns a new {@link FakeData} with the given {@code uri}. */ + public FakeData newData(String uri) { + FakeData data = new FakeData(this, uri); + dataMap.put(uri, data); + return data; + } + + /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */ + public FakeData getData(String uri) { + FakeData data = dataMap.get(uri); + return data != null ? data : defaultData; + } + + /** Returns a list of all data including {@code defaultData}. */ + public ArrayList getAllData() { + ArrayList fakeDatas = new ArrayList<>(dataMap.values()); + if (defaultData != null) { + fakeDatas.add(defaultData); + } + return fakeDatas; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 3e4d6b0440..6180a8aa77 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -16,59 +16,50 @@ package com.google.android.exoplayer2.testutil; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; /** * A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet} * instance which determines the response to data access calls. - * - *

      Multiple fake data can be defined by {@link FakeDataSet#setData(String, byte[])} and {@link - * FakeDataSet#newData(String)} methods. It's also possible to define a default data by {@link - * FakeDataSet#newDefaultData()}. - * - *

      {@link FakeDataSet#newData(String)} and {@link FakeDataSet#newDefaultData()} return a {@link - * FakeData} instance which can be used to define specific results during {@link #read(byte[], int, - * int)} calls. - * - *

      The data that will be read from the source can be constructed by calling {@link - * FakeData#appendReadData(byte[])} Calls to {@link #read(byte[], int, int)} will not span the - * boundaries between arrays passed to successive calls, and hence the boundaries control the - * positions at which read requests to the source may only be partially satisfied. - * - *

      Errors can be inserted by calling {@link FakeData#appendReadError(IOException)}. An inserted - * error will be thrown from the first call to {@link #read(byte[], int, int)} that attempts to read - * from the corresponding position, and from all subsequent calls to {@link #read(byte[], int, int)} - * until the source is closed. If the source is closed and re-opened having encountered an error, - * that error will not be thrown again. - * - *

      Example usage: - * - *

      - *   // Create a FakeDataSource then add default data and two FakeData
      - *   // "test_file" throws an IOException when tried to be read until closed and reopened.
      - *   FakeDataSource fakeDataSource = new FakeDataSource();
      - *   fakeDataSource.getDataSet()
      - *       .newDefaultData()
      - *         .appendReadData(defaultData)
      - *         .endData()
      - *       .setData("http:///1", data1)
      - *       .newData("test_file")
      - *         .appendReadError(new IOException())
      - *         .appendReadData(data2);
      - *    // No need to call endData at the end
      - * 
      */ -public final class FakeDataSource implements DataSource { +public class FakeDataSource implements DataSource { + + /** + * Factory to create a {@link FakeDataSource}. + */ + public static class Factory implements DataSource.Factory { + + protected final TransferListener transferListener; + protected FakeDataSet fakeDataSet; + + public Factory(@Nullable TransferListener transferListener) { + this.transferListener = transferListener; + } + + public final Factory setFakeDataSet(FakeDataSet fakeDataSet) { + this.fakeDataSet = fakeDataSet; + return this; + } + + @Override + public DataSource createDataSource() { + return new FakeDataSource(fakeDataSet, transferListener); + } + + } private final FakeDataSet fakeDataSet; + private final TransferListener transferListener; private final ArrayList openedDataSpecs; private Uri uri; @@ -77,30 +68,28 @@ public final class FakeDataSource implements DataSource { private int currentSegmentIndex; private long bytesRemaining; - public static Factory newFactory(final FakeDataSet fakeDataSet) { - return new Factory() { - @Override - public DataSource createDataSource() { - return new FakeDataSource(fakeDataSet); - } - }; - } - public FakeDataSource() { this(new FakeDataSet()); } public FakeDataSource(FakeDataSet fakeDataSet) { + this(fakeDataSet, null); + } + + public FakeDataSource(FakeDataSet fakeDataSet, + @Nullable TransferListener transferListener) { + Assertions.checkNotNull(fakeDataSet); this.fakeDataSet = fakeDataSet; + this.transferListener = transferListener; this.openedDataSpecs = new ArrayList<>(); } - public FakeDataSet getDataSet() { + public final FakeDataSet getDataSet() { return fakeDataSet; } @Override - public long open(DataSpec dataSpec) throws IOException { + public final long open(DataSpec dataSpec) throws IOException { Assertions.checkState(!opened); // DataSpec requires a matching close call even if open fails. opened = true; @@ -113,7 +102,7 @@ public final class FakeDataSource implements DataSource { } long totalLength = 0; - for (Segment segment : fakeData.segments) { + for (Segment segment : fakeData.getSegments()) { totalLength += segment.length; } @@ -130,20 +119,23 @@ public final class FakeDataSource implements DataSource { boolean findingCurrentSegmentIndex = true; currentSegmentIndex = 0; int scannedLength = 0; - for (Segment segment : fakeData.segments) { + for (Segment segment : fakeData.getSegments()) { segment.bytesRead = (int) Math.min(Math.max(0, dataSpec.position - scannedLength), segment.length); scannedLength += segment.length; findingCurrentSegmentIndex &= segment.isErrorSegment() ? segment.exceptionCleared - : segment.bytesRead == segment.length; + : (!segment.isActionSegment() && segment.bytesRead == segment.length); if (findingCurrentSegmentIndex) { currentSegmentIndex++; } } + if (transferListener != null) { + transferListener.onTransferStart(this, dataSpec); + } // Configure bytesRemaining, and return. if (dataSpec.length == C.LENGTH_UNSET) { bytesRemaining = totalLength - dataSpec.position; - return fakeData.simulateUnknownLength ? C.LENGTH_UNSET : bytesRemaining; + return fakeData.isSimulatingUnknownLength() ? C.LENGTH_UNSET : bytesRemaining; } else { bytesRemaining = dataSpec.length; return bytesRemaining; @@ -151,13 +143,13 @@ public final class FakeDataSource implements DataSource { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public final int read(byte[] buffer, int offset, int readLength) throws IOException { Assertions.checkState(opened); while (true) { - if (currentSegmentIndex == fakeData.segments.size() || bytesRemaining == 0) { + if (currentSegmentIndex == fakeData.getSegments().size() || bytesRemaining == 0) { return C.RESULT_END_OF_INPUT; } - Segment current = fakeData.segments.get(currentSegmentIndex); + Segment current = fakeData.getSegments().get(currentSegmentIndex); if (current.isErrorSegment()) { if (!current.exceptionCleared) { current.exceptionThrown = true; @@ -165,13 +157,22 @@ public final class FakeDataSource implements DataSource { } else { currentSegmentIndex++; } + } else if (current.isActionSegment()) { + currentSegmentIndex++; + current.action.run(); } else { // Read at most bytesRemaining. readLength = (int) Math.min(readLength, bytesRemaining); // Do not allow crossing of the segment boundary. readLength = Math.min(readLength, current.length - current.bytesRead); // Perform the read and return. - System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); + if (current.data != null) { + System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); + } + onDataRead(readLength); + if (transferListener != null) { + transferListener.onBytesTransferred(this, readLength); + } bytesRemaining -= readLength; current.bytesRead += readLength; if (current.bytesRead == current.length) { @@ -183,21 +184,24 @@ public final class FakeDataSource implements DataSource { } @Override - public Uri getUri() { + public final Uri getUri() { return uri; } @Override - public void close() throws IOException { + public final void close() throws IOException { Assertions.checkState(opened); opened = false; uri = null; - if (currentSegmentIndex < fakeData.segments.size()) { - Segment current = fakeData.segments.get(currentSegmentIndex); + if (fakeData != null && currentSegmentIndex < fakeData.getSegments().size()) { + Segment current = fakeData.getSegments().get(currentSegmentIndex); if (current.isErrorSegment() && current.exceptionThrown) { current.exceptionCleared = true; } } + if (transferListener != null) { + transferListener.onTransferEnd(this); + } fakeData = null; } @@ -205,136 +209,15 @@ public final class FakeDataSource implements DataSource { * Returns the {@link DataSpec} instances passed to {@link #open(DataSpec)} since the last call to * this method. */ - public DataSpec[] getAndClearOpenedDataSpecs() { + public final DataSpec[] getAndClearOpenedDataSpecs() { DataSpec[] dataSpecs = new DataSpec[openedDataSpecs.size()]; openedDataSpecs.toArray(dataSpecs); openedDataSpecs.clear(); return dataSpecs; } - private static class Segment { - - public final IOException exception; - public final byte[] data; - public final int length; - - private boolean exceptionThrown; - private boolean exceptionCleared; - private int bytesRead; - - public Segment(byte[] data, IOException exception) { - this.data = data; - this.exception = exception; - length = data != null ? data.length : 0; - } - - public boolean isErrorSegment() { - return exception != null; - } - - } - - /** Container of fake data to be served by a {@link FakeDataSource}. */ - public static final class FakeData { - - /** Uri of the data or null if this is the default FakeData. */ - public final String uri; - private final ArrayList segments; - private final FakeDataSet dataSet; - private boolean simulateUnknownLength; - - private FakeData(FakeDataSet dataSet, String uri) { - this.uri = uri; - this.segments = new ArrayList<>(); - this.dataSet = dataSet; - } - - /** Returns the {@link FakeDataSet} this FakeData belongs to. */ - public FakeDataSet endData() { - return dataSet; - } - - /** - * When set, {@link FakeDataSource#open(DataSpec)} will behave as though the source is unable to - * determine the length of the underlying data. Hence the return value will always be equal to - * the {@link DataSpec#length} of the argument, including the case where the length is equal to - * {@link C#LENGTH_UNSET}. - */ - public FakeData setSimulateUnknownLength(boolean simulateUnknownLength) { - this.simulateUnknownLength = simulateUnknownLength; - return this; - } - - /** - * Appends to the underlying data. - */ - public FakeData appendReadData(byte[] data) { - Assertions.checkState(data != null && data.length > 0); - segments.add(new Segment(data, null)); - return this; - } - - /** - * Appends an error in the underlying data. - */ - public FakeData appendReadError(IOException exception) { - segments.add(new Segment(null, exception)); - return this; - } - - /** Returns the whole data added by {@link #appendReadData(byte[])}. */ - public byte[] getData() { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - for (Segment segment : segments) { - if (segment.data != null) { - try { - outputStream.write(segment.data); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - } - return outputStream.toByteArray(); - } - } - - /** A set of {@link FakeData} instances. */ - public static final class FakeDataSet { - - private final HashMap dataMap; - private FakeData defaultData; - - public FakeDataSet() { - dataMap = new HashMap<>(); - } - - public FakeData newDefaultData() { - defaultData = new FakeData(this, null); - return defaultData; - } - - public FakeData newData(String uri) { - FakeData data = new FakeData(this, uri); - dataMap.put(uri, data); - return data; - } - - public FakeDataSet setData(String uri, byte[] data) { - return newData(uri).appendReadData(data).endData(); - } - - public FakeData getData(String uri) { - FakeData data = dataMap.get(uri); - return data != null ? data : defaultData; - } - - public ArrayList getAllData() { - ArrayList fakeDatas = new ArrayList<>(dataMap.values()); - if (defaultData != null) { - fakeDatas.add(defaultData); - } - return fakeDatas; - } + protected void onDataRead(int bytesRead) { + // Do nothing. Can be overridden. } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java new file mode 100644 index 0000000000..4d118f9288 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java @@ -0,0 +1,36 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.util.MediaClock; + +/** + * Fake abstract {@link Renderer} which is also a {@link MediaClock}. + */ +public abstract class FakeMediaClockRenderer extends FakeRenderer implements MediaClock { + + public FakeMediaClockRenderer(Format... expectedFormats) { + super(expectedFormats); + } + + @Override + public MediaClock getMediaClock() { + return this; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java new file mode 100644 index 0000000000..d8e501a298 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -0,0 +1,126 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.io.IOException; +import junit.framework.Assert; + +/** + * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that + * track will give the player a {@link FakeSampleStream}. + */ +public 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, long positionUs) { + Assert.assertFalse(preparedPeriod); + Assert.assertEquals(0, positionUs); + preparedPeriod = true; + callback.onPrepared(this); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + Assert.assertTrue(preparedPeriod); + } + + @Override + public TrackGroupArray getTrackGroups() { + Assert.assertTrue(preparedPeriod); + return trackGroupArray; + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + Assert.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]; + Assert.assertTrue(1 <= selection.length()); + TrackGroup trackGroup = selection.getTrackGroup(); + Assert.assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET); + int indexInTrackGroup = selection.getIndexInTrackGroup(selection.getSelectedIndex()); + Assert.assertTrue(0 <= indexInTrackGroup); + Assert.assertTrue(indexInTrackGroup < trackGroup.length); + streams[i] = new FakeSampleStream(selection.getSelectedFormat()); + streamResetFlags[i] = true; + } + } + return 0; + } + + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + + @Override + public long readDiscontinuity() { + Assert.assertTrue(preparedPeriod); + return C.TIME_UNSET; + } + + @Override + public long getBufferedPositionUs() { + Assert.assertTrue(preparedPeriod); + return C.TIME_END_OF_SOURCE; + } + + @Override + public long seekToUs(long positionUs) { + Assert.assertTrue(preparedPeriod); + return positionUs; + } + + @Override + public long getNextLoadPositionUs() { + Assert.assertTrue(preparedPeriod); + return C.TIME_END_OF_SOURCE; + } + + @Override + public boolean continueLoading(long positionUs) { + Assert.assertTrue(preparedPeriod); + return false; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java new file mode 100644 index 0000000000..a2c1e9879e --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -0,0 +1,114 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import junit.framework.Assert; + +/** + * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a + * {@link FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s. + */ +public class FakeMediaSource implements MediaSource { + + private final Timeline timeline; + private final Object manifest; + private final TrackGroupArray trackGroupArray; + private final ArrayList activeMediaPeriods; + + private boolean preparedSource; + private boolean releasedSource; + + /** + * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a + * {@link TrackGroupArray} using the given {@link Format}s. + */ + public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { + this(timeline, manifest, buildTrackGroupArray(formats)); + } + + /** + * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with the + * given {@link TrackGroupArray}. + */ + public FakeMediaSource(Timeline timeline, Object manifest, TrackGroupArray trackGroupArray) { + this.timeline = timeline; + this.manifest = manifest; + this.activeMediaPeriods = new ArrayList<>(); + this.trackGroupArray = trackGroupArray; + } + + public void assertReleased() { + Assert.assertTrue(releasedSource); + } + + @Override + public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assert.assertFalse(preparedSource); + preparedSource = true; + listener.onSourceInfoRefreshed(timeline, manifest); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + Assert.assertTrue(preparedSource); + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); + Assert.assertTrue(preparedSource); + Assert.assertFalse(releasedSource); + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + activeMediaPeriods.add(mediaPeriod); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + Assert.assertTrue(preparedSource); + Assert.assertFalse(releasedSource); + FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod; + Assert.assertTrue(activeMediaPeriods.remove(fakeMediaPeriod)); + fakeMediaPeriod.release(); + } + + @Override + public void releaseSource() { + Assert.assertTrue(preparedSource); + Assert.assertFalse(releasedSource); + Assert.assertTrue(activeMediaPeriods.isEmpty()); + releasedSource = true; + } + + private static TrackGroupArray buildTrackGroupArray(Format... formats) { + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < formats.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + return new TrackGroupArray(trackGroups); + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java new file mode 100644 index 0000000000..a66043b77f --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -0,0 +1,94 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.BaseRenderer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import junit.framework.Assert; + +/** + * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer + * verifies that it reads one of the given {@link Format}s. + */ +public class FakeRenderer extends BaseRenderer { + + private final List expectedFormats; + private final DecoderInputBuffer buffer; + + public int positionResetCount; + public int formatReadCount; + public int bufferReadCount; + public boolean isEnded; + public boolean isReady; + + public FakeRenderer(Format... expectedFormats) { + super(expectedFormats.length == 0 ? C.TRACK_TYPE_UNKNOWN + : MimeTypes.getTrackType(expectedFormats[0].sampleMimeType)); + this.expectedFormats = Collections.unmodifiableList(Arrays.asList(expectedFormats)); + this.buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); + } + + @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) { + // Verify the format matches the expected format. + FormatHolder formatHolder = new FormatHolder(); + int result = readSource(formatHolder, buffer, false); + if (result == C.RESULT_FORMAT_READ) { + formatReadCount++; + Assert.assertTrue(expectedFormats.contains(formatHolder.format)); + } else if (result == C.RESULT_BUFFER_READ) { + bufferReadCount++; + if (buffer.isEndOfStream()) { + isEnded = true; + } + } + } + isReady = buffer.timeUs >= positionUs; + } + + @Override + public boolean isReady() { + return isReady || isSourceReady(); + } + + @Override + public boolean isEnded() { + return isEnded; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) + ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java new file mode 100644 index 0000000000..4e1e32980f --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -0,0 +1,67 @@ +/* + * 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.testutil; + +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.source.SampleStream; +import java.io.IOException; + +/** + * Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag + * on its input buffer. + */ +public 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. + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java new file mode 100644 index 0000000000..040782264b --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -0,0 +1,111 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Util; + +/** + * Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s. + */ +public final class FakeTimeline extends Timeline { + + /** + * Definition used to define a {@link FakeTimeline}. + */ + public static final class TimelineWindowDefinition { + + private static final int WINDOW_DURATION_US = 100000; + + public final int periodCount; + public final Object id; + public final boolean isSeekable; + public final boolean isDynamic; + public final long durationUs; + + public TimelineWindowDefinition(int periodCount, Object id) { + this(periodCount, id, true, false, WINDOW_DURATION_US); + } + + public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { + this(1, 0, isSeekable, isDynamic, durationUs); + } + + public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, + boolean isDynamic, long durationUs) { + this.periodCount = periodCount; + this.id = id; + this.isSeekable = isSeekable; + this.isDynamic = isDynamic; + this.durationUs = durationUs; + } + + } + + private final TimelineWindowDefinition[] windowDefinitions; + private final int[] periodOffsets; + + public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { + this.windowDefinitions = windowDefinitions; + periodOffsets = new int[windowDefinitions.length + 1]; + periodOffsets[0] = 0; + for (int i = 0; i < windowDefinitions.length; i++) { + periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount; + } + } + + @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 ? windowDefinition.id : null; + return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable, + windowDefinition.isDynamic, 0, windowDefinition.durationUs, periodOffsets[windowIndex], + periodOffsets[windowIndex + 1] - 1, 0); + } + + @Override + public int getPeriodCount() { + return periodOffsets[periodOffsets.length - 1]; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + int windowIndex = Util.binarySearchFloor(periodOffsets, periodIndex, true, false); + int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; + TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; + Object id = setIds ? windowPeriodIndex : null; + Object uid = setIds ? periodIndex : null; + long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; + return period.set(id, uid, windowIndex, periodDurationUs, periodDurationUs * windowPeriodIndex); + } + + @Override + public int getIndexOfPeriod(Object uid) { + if (!(uid instanceof Integer)) { + return C.INDEX_UNSET; + } + int index = (Integer) uid; + return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java index b399d79e8d..b14e6f60ef 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java @@ -36,7 +36,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { private final ArrayList sampleFlags; private final ArrayList sampleStartOffsets; private final ArrayList sampleEndOffsets; - private final ArrayList sampleEncryptionKeys; + private final ArrayList cryptoDatas; private byte[] sampleData; public Format format; @@ -47,7 +47,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { sampleFlags = new ArrayList<>(); sampleStartOffsets = new ArrayList<>(); sampleEndOffsets = new ArrayList<>(); - sampleEncryptionKeys = new ArrayList<>(); + cryptoDatas = new ArrayList<>(); } public void clear() { @@ -56,7 +56,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { sampleFlags.clear(); sampleStartOffsets.clear(); sampleEndOffsets.clear(); - sampleEncryptionKeys.clear(); + cryptoDatas.clear(); } @Override @@ -89,29 +89,24 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { + CryptoData cryptoData) { sampleTimesUs.add(timeUs); sampleFlags.add(flags); sampleStartOffsets.add(sampleData.length - offset - size); sampleEndOffsets.add(sampleData.length - offset); - sampleEncryptionKeys.add(encryptionKey); + cryptoDatas.add(cryptoData); } public void assertSampleCount(int count) { Assert.assertEquals(count, sampleTimesUs.size()); } - public void assertSample(int index, byte[] data, long timeUs, int flags, byte[] encryptionKey) { + public void assertSample(int index, byte[] data, long timeUs, int flags, CryptoData cryptoData) { byte[] actualData = getSampleData(index); MoreAsserts.assertEquals(data, actualData); Assert.assertEquals(timeUs, (long) sampleTimesUs.get(index)); Assert.assertEquals(flags, (int) sampleFlags.get(index)); - byte[] sampleEncryptionKey = sampleEncryptionKeys.get(index); - if (encryptionKey == null) { - Assert.assertEquals(null, sampleEncryptionKey); - } else { - MoreAsserts.assertEquals(encryptionKey, sampleEncryptionKey); - } + Assert.assertEquals(cryptoData, cryptoDatas.get(index)); } public byte[] getSampleData(int index) { @@ -128,10 +123,10 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { Assert.assertEquals(expected.sampleFlags.get(i), sampleFlags.get(i)); Assert.assertEquals(expected.sampleStartOffsets.get(i), sampleStartOffsets.get(i)); Assert.assertEquals(expected.sampleEndOffsets.get(i), sampleEndOffsets.get(i)); - if (expected.sampleEncryptionKeys.get(i) == null) { - Assert.assertNull(sampleEncryptionKeys.get(i)); + if (expected.cryptoDatas.get(i) == null) { + Assert.assertNull(cryptoDatas.get(i)); } else { - MoreAsserts.assertEquals(expected.sampleEncryptionKeys.get(i), sampleEncryptionKeys.get(i)); + Assert.assertEquals(expected.cryptoDatas.get(i), cryptoDatas.get(i)); } } } @@ -172,9 +167,10 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { .add("time", sampleTimesUs.get(i)) .add("flags", sampleFlags.get(i)) .add("data", getSampleData(i)); - byte[] key = sampleEncryptionKeys.get(i); - if (key != null) { - dumper.add("encryption key", key); + CryptoData cryptoData = cryptoDatas.get(i); + if (cryptoData != null) { + dumper.add("crypto mode", cryptoData.cryptoMode); + dumper.add("encryption key", cryptoData.encryptionKey); } dumper.endBlock(); } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java similarity index 90% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 9c2ced3a8a..831344aa8b 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.fail; @@ -32,7 +32,6 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.Window; -import com.google.android.exoplayer2.playbacktests.R; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -102,6 +101,17 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba * is exceeded then the test will fail. */ public void runTest(final HostedTest hostedTest, long timeoutMs) { + runTest(hostedTest, timeoutMs, true); + } + + /** + * Executes a {@link HostedTest} inside the host. + * + * @param hostedTest The test to execute. + * @param timeoutMs The number of milliseconds to wait for the test to finish. + * @param failOnTimeout Whether the test fails when the timeout is exceeded. + */ + public void runTest(final HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { Assertions.checkArgument(timeoutMs > 0); Assertions.checkState(Thread.currentThread() != getMainLooper().getThread()); @@ -132,7 +142,11 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba } else { String message = "Test timed out after " + timeoutMs + " ms."; Log.e(TAG, message); - fail(message); + if (failOnTimeout) { + fail(message); + } + maybeStopHostedTest(); + hostedTestStoppedCondition.block(); } } @@ -142,8 +156,9 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.host_activity); - surfaceView = (SurfaceView) findViewById(R.id.surface_view); + setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName())); + surfaceView = (SurfaceView) findViewById( + getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); mainHandler = new Handler(); checkCanStopRunnable = new CheckCanStopRunnable(); diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/LogcatMetricsLogger.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java similarity index 95% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/LogcatMetricsLogger.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java index 4c44f77143..fdff47dd2c 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/LogcatMetricsLogger.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/LogcatMetricsLogger.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.util.Log; diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/MetricsLogger.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java similarity index 97% rename from playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/MetricsLogger.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java index 6e36ff728f..64d1944927 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/MetricsLogger.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MetricsLogger.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.playbacktests.util; +package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 363f60b10d..5819a4b711 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -19,9 +19,10 @@ import android.app.Instrumentation; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -39,23 +40,8 @@ import org.mockito.MockitoAnnotations; */ public class TestUtil { - /** - * A factory for {@link Extractor} instances. - */ - public interface ExtractorFactory { - Extractor create(); - } - - private static final String DUMP_EXTENSION = ".dump"; - private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; - private TestUtil() {} - public static boolean sniffTestData(Extractor extractor, byte[] data) - throws IOException, InterruptedException { - return sniffTestData(extractor, newExtractorInput(data)); - } - public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input) throws IOException, InterruptedException { while (true) { @@ -83,54 +69,6 @@ public class TestUtil { return Arrays.copyOf(data, position); } - public static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input, - long timeUs) throws IOException, InterruptedException { - return consumeTestData(extractor, input, timeUs, false); - } - - public static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input, - long timeUs, boolean retryFromStartIfLive) throws IOException, InterruptedException { - FakeExtractorOutput output = new FakeExtractorOutput(); - extractor.init(output); - consumeTestData(extractor, input, timeUs, output, retryFromStartIfLive); - return output; - } - - private static void consumeTestData(Extractor extractor, FakeExtractorInput input, long timeUs, - FakeExtractorOutput output, boolean retryFromStartIfLive) - throws IOException, InterruptedException { - extractor.seek(input.getPosition(), timeUs); - PositionHolder seekPositionHolder = new PositionHolder(); - int readResult = Extractor.RESULT_CONTINUE; - while (readResult != Extractor.RESULT_END_OF_INPUT) { - try { - // Extractor.read should not read seekPositionHolder.position. Set it to a value that's - // likely to cause test failure if a read does occur. - seekPositionHolder.position = Long.MIN_VALUE; - readResult = extractor.read(input, seekPositionHolder); - if (readResult == Extractor.RESULT_SEEK) { - long seekPosition = seekPositionHolder.position; - Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE); - input.setPosition((int) seekPosition); - } - } catch (SimulatedIOException e) { - if (!retryFromStartIfLive) { - continue; - } - boolean isOnDemand = input.getLength() != C.LENGTH_UNSET - || (output.seekMap != null && output.seekMap.getDurationUs() != C.TIME_UNSET); - if (isOnDemand) { - continue; - } - input.setPosition(0); - for (int i = 0; i < output.numberOfTracks; i++) { - output.trackOutputs.valueAt(i).clear(); - } - extractor.seek(0, 0); - } - } - } - public static byte[] buildTestData(int length) { return buildTestData(length, length); } @@ -190,15 +128,6 @@ public class TestUtil { MockitoAnnotations.initMocks(instrumentationTestCase); } - public static boolean assetExists(Instrumentation instrumentation, String fileName) - throws IOException { - int i = fileName.lastIndexOf('/'); - String path = i >= 0 ? fileName.substring(0, i) : ""; - String file = i >= 0 ? fileName.substring(i + 1) : fileName; - return Arrays.asList(instrumentation.getContext().getResources().getAssets().list(path)) - .contains(file); - } - public static byte[] getByteArray(Instrumentation instrumentation, String fileName) throws IOException { return Util.toByteArray(getInputStream(instrumentation, fileName)); @@ -214,182 +143,30 @@ public class TestUtil { return new String(getByteArray(instrumentation, fileName)); } - private static FakeExtractorInput newExtractorInput(byte[] data) { - return new FakeExtractorInput.Builder().setData(data).build(); - } - /** - * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param instrumentation To be used to load the sample file. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) + * Extracts the timeline from a media source. */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, - Instrumentation instrumentation) throws IOException, InterruptedException { - byte[] fileData = getByteArray(instrumentation, sampleFile); - assertOutput(factory, sampleFile, fileData, instrumentation); - } - - /** - * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. - * @param instrumentation To be used to load the sample file. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) - */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, byte[] fileData, - Instrumentation instrumentation) throws IOException, InterruptedException { - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, true); - } - - /** - * Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals - * to a prerecorded output dump file with the name {@code sampleFile} + "{@value - * #DUMP_EXTENSION}". If {@code simulateUnknownLength} is true and {@code sampleFile} + "{@value - * #UNKNOWN_LENGTH_EXTENSION}" exists, it's preferred. - * - * @param extractor The {@link Extractor} to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. - * @param instrumentation To be used to load the sample file. - * @param simulateIOErrors If true simulates IOErrors. - * @param simulateUnknownLength If true simulates unknown input length. - * @param simulatePartialReads If true simulates partial reads. - * @return The {@link FakeExtractorOutput} used in the test. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - */ - public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, - byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, - boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, - InterruptedException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) - .setSimulateIOErrors(simulateIOErrors) - .setSimulateUnknownLength(simulateUnknownLength) - .setSimulatePartialReads(simulatePartialReads).build(); - - Assert.assertTrue(sniffTestData(extractor, input)); - input.resetPeekPosition(); - FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); - - if (simulateUnknownLength - && assetExists(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION)) { - extractorOutput.assertOutput(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION); - } else { - extractorOutput.assertOutput(instrumentation, sampleFile + ".0" + DUMP_EXTENSION); + public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { + class TimelineListener implements Listener { + private Timeline timeline; + @Override + public synchronized void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + this.timeline = timeline; + this.notify(); + } } - - SeekMap seekMap = extractorOutput.seekMap; - if (seekMap.isSeekable()) { - long durationUs = seekMap.getDurationUs(); - for (int j = 0; j < 4; j++) { - long timeUs = (durationUs * j) / 3; - long position = seekMap.getPosition(timeUs); - input.setPosition((int) position); - for (int i = 0; i < extractorOutput.numberOfTracks; i++) { - extractorOutput.trackOutputs.valueAt(i).clear(); + TimelineListener listener = new TimelineListener(); + mediaSource.prepareSource(null, true, listener); + synchronized (listener) { + while (listener.timeline == null) { + try { + listener.wait(); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); } - - consumeTestData(extractor, input, timeUs, extractorOutput, false); - extractorOutput.assertOutput(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION); } } - - return extractorOutput; - } - - /** - * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all - * possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param instrumentation To be used to load the sample file. - * @param expectedThrowable Expected {@link Throwable} class. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) - */ - public static void assertThrows(ExtractorFactory factory, String sampleFile, - Instrumentation instrumentation, Class expectedThrowable) - throws IOException, InterruptedException { - byte[] fileData = getByteArray(instrumentation, sampleFile); - assertThrows(factory, fileData, expectedThrowable); - } - - /** - * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all - * possible combinations of "simulate" parameters. - * - * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} - * class which is to be tested. - * @param fileData Content of the input file. - * @param expectedThrowable Expected {@link Throwable} class. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) - */ - public static void assertThrows(ExtractorFactory factory, byte[] fileData, - Class expectedThrowable) throws IOException, InterruptedException { - assertThrows(factory.create(), fileData, expectedThrowable, false, false, false); - assertThrows(factory.create(), fileData, expectedThrowable, true, false, false); - assertThrows(factory.create(), fileData, expectedThrowable, false, true, false); - assertThrows(factory.create(), fileData, expectedThrowable, true, true, false); - assertThrows(factory.create(), fileData, expectedThrowable, false, false, true); - assertThrows(factory.create(), fileData, expectedThrowable, true, false, true); - assertThrows(factory.create(), fileData, expectedThrowable, false, true, true); - assertThrows(factory.create(), fileData, expectedThrowable, true, true, true); - } - - /** - * Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code sampleFile}. - * - * @param extractor The {@link Extractor} to be tested. - * @param fileData Content of the input file. - * @param expectedThrowable Expected {@link Throwable} class. - * @param simulateIOErrors If true simulates IOErrors. - * @param simulateUnknownLength If true simulates unknown input length. - * @param simulatePartialReads If true simulates partial reads. - * @throws IOException If reading from the input fails. - * @throws InterruptedException If interrupted while reading from the input. - */ - public static void assertThrows(Extractor extractor, byte[] fileData, - Class expectedThrowable, boolean simulateIOErrors, - boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, - InterruptedException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) - .setSimulateIOErrors(simulateIOErrors) - .setSimulateUnknownLength(simulateUnknownLength) - .setSimulatePartialReads(simulatePartialReads).build(); - try { - consumeTestData(extractor, input, 0, true); - throw new AssertionError(expectedThrowable.getSimpleName() + " expected but not thrown"); - } catch (Throwable throwable) { - if (expectedThrowable.equals(throwable.getClass())) { - return; // Pass! - } - throw throwable; - } + return listener.timeline; } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java new file mode 100644 index 0000000000..8357ce70c7 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -0,0 +1,148 @@ +/* + * 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.testutil; + +import static junit.framework.Assert.assertEquals; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; + +/** + * Unit test for {@link Timeline}. + */ +public final class TimelineAsserts { + + private TimelineAsserts() {} + + /** + * Assert that timeline is empty (i.e. has no windows or periods). + */ + public static void assertEmpty(Timeline timeline) { + assertWindowIds(timeline); + assertPeriodCounts(timeline); + } + + /** + * Asserts that window IDs are set correctly. + * + * @param expectedWindowIds A list of expected window IDs. If an ID is unknown or not important + * {@code null} can be passed to skip this window. + */ + public static void assertWindowIds(Timeline timeline, Object... expectedWindowIds) { + Window window = new Window(); + assertEquals(expectedWindowIds.length, timeline.getWindowCount()); + for (int i = 0; i < timeline.getWindowCount(); i++) { + timeline.getWindow(i, window, true); + if (expectedWindowIds[i] != null) { + assertEquals(expectedWindowIds[i], window.id); + } + } + } + + /** + * Asserts that window properties {@link Window}.isDynamic are set correctly.. + */ + public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { + Window window = new Window(); + for (int i = 0; i < timeline.getWindowCount(); i++) { + timeline.getWindow(i, window, true); + assertEquals(windowIsDynamic[i], window.isDynamic); + } + } + + /** + * Asserts that previous window indices for each window are set correctly depending on the repeat + * mode. + */ + public static void assertPreviousWindowIndices(Timeline timeline, + @Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertEquals(expectedPreviousWindowIndices[i], + timeline.getPreviousWindowIndex(i, repeatMode)); + } + } + + /** + * Asserts that next window indices for each window are set correctly depending on the repeat + * mode. + */ + public static void assertNextWindowIndices(Timeline timeline, @Player.RepeatMode int repeatMode, + int... expectedNextWindowIndices) { + for (int i = 0; i < timeline.getWindowCount(); i++) { + assertEquals(expectedNextWindowIndices[i], + timeline.getNextWindowIndex(i, repeatMode)); + } + } + + /** + * Asserts that period counts for each window are set correctly. Also asserts that + * {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it + * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int)}. + */ + public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { + int windowCount = timeline.getWindowCount(); + int[] accumulatedPeriodCounts = new int[windowCount + 1]; + accumulatedPeriodCounts[0] = 0; + for (int i = 0; i < windowCount; i++) { + accumulatedPeriodCounts[i + 1] = accumulatedPeriodCounts[i] + expectedPeriodCounts[i]; + } + assertEquals(accumulatedPeriodCounts[accumulatedPeriodCounts.length - 1], + timeline.getPeriodCount()); + Window window = new Window(); + Period period = new Period(); + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window, true); + assertEquals(accumulatedPeriodCounts[i], window.firstPeriodIndex); + assertEquals(accumulatedPeriodCounts[i + 1] - 1, window.lastPeriodIndex); + } + int expectedWindowIndex = 0; + for (int i = 0; i < timeline.getPeriodCount(); i++) { + timeline.getPeriod(i, period, true); + while (i >= accumulatedPeriodCounts[expectedWindowIndex + 1]) { + expectedWindowIndex++; + } + assertEquals(expectedWindowIndex, period.windowIndex); + if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL)); + } else { + int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, + Player.REPEAT_MODE_OFF); + int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, + Player.REPEAT_MODE_ONE); + int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, + Player.REPEAT_MODE_ALL); + int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowOff]; + int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowOne]; + int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindowAll]; + assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, + Player.REPEAT_MODE_OFF)); + assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, + Player.REPEAT_MODE_ONE)); + assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, + Player.REPEAT_MODE_ALL)); + } + } + } + +} diff --git a/playbacktests/src/main/res/layout/host_activity.xml b/testutils/src/main/res/layout/host_activity.xml similarity index 100% rename from playbacktests/src/main/res/layout/host_activity.xml rename to testutils/src/main/res/layout/host_activity.xml