Commit deb9b301 by Oliver Woodman

Merge branch 'dev-v2-r2.5.0' into release-v2

parents cbffc14f 7a109f74
Showing with 2401 additions and 151 deletions
...@@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates. ...@@ -20,6 +20,11 @@ and extend, and can be updated through Play Store application updates.
## Using ExoPlayer ## ## Using ExoPlayer ##
ExoPlayer modules can be obtained via jCenter. It's also possible to clone the
repository and depend on the modules locally.
### Via jCenter ###
The easiest way to get started using ExoPlayer is to add it as a gradle The easiest way to get started using ExoPlayer is to add it as a gradle
dependency. You need to make sure you have the jcenter repository included in dependency. You need to make sure you have the jcenter repository included in
the `build.gradle` file in the root of your project: the `build.gradle` file in the root of your project:
...@@ -64,6 +69,39 @@ latest versions, see the [Release notes][]. ...@@ -64,6 +69,39 @@ latest versions, see the [Release notes][].
[Bintray]: https://bintray.com/google/exoplayer [Bintray]: https://bintray.com/google/exoplayer
[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md [Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
### Locally ###
Cloning the repository and depending on the modules locally is required when
using some ExoPlayer extension modules. It's also a suitable approach if you
want to make local changes to ExoPlayer, or if you want to use a development
branch.
First, clone the repository into a local directory and checkout the desired
branch:
```sh
git clone https://github.com/google/ExoPlayer.git
git checkout release-v2
```
Next, add the following to your project's `settings.gradle` file, replacing
`path/to/exoplayer` with the path to your local copy:
```gradle
gradle.ext.exoplayerRoot = 'path/to/exoplayer'
gradle.ext.exoplayerModulePrefix = 'exoplayer-'
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle')
```
You should now see the ExoPlayer modules appear as part of your project. You can
depend on them as you would on any other local module, for example:
```gradle
compile project(':exoplayer-library-core')
compile project(':exoplayer-library-dash')
compile project(':exoplayer-library-ui)
```
## Developing ExoPlayer ## ## Developing ExoPlayer ##
#### Project branches #### #### Project branches ####
......
# Release notes # # 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 ### ### r2.4.4 ###
* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance * HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance
......
...@@ -16,8 +16,8 @@ buildscript { ...@@ -16,8 +16,8 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'com.novoda:bintray-release:0.4.0' classpath 'com.novoda:bintray-release:0.5.0'
} }
// Workaround for the following test coverage issue. Remove when fixed: // Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070 // https://code.google.com/p/android/issues/detail?id=226070
...@@ -31,25 +31,12 @@ buildscript { ...@@ -31,25 +31,12 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
jcenter() jcenter()
maven {
url "https://maven.google.com"
}
} }
project.ext { project.ext {
// Important: ExoPlayer specifies a minSdkVersion of 9 because various exoplayerPublishEnabled = true
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality
// provided by the library requires API level 16 or greater.
minSdkVersion = 9
compileSdkVersion = 25
targetSdkVersion = 25
buildToolsVersion = '25'
testSupportLibraryVersion = '0.5'
supportLibraryVersion = '25.3.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.4.4'
releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
if (it.hasProperty('externalBuildDir')) { if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) { if (!new File(externalBuildDir).isAbsolute()) {
...@@ -59,10 +46,4 @@ allprojects { ...@@ -59,10 +46,4 @@ allprojects {
} }
} }
def getBintrayRepo() {
boolean publicRepo = hasProperty('publicRepo') &&
property('publicRepo').toBoolean()
return publicRepo ? 'exoplayer' : 'exoplayer-test'
}
apply from: 'javadoc_combined.gradle' apply from: 'javadoc_combined.gradle'
// 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
}
}
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
def rootDir = gradle.ext.exoplayerRoot
def modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
}
include modulePrefix + 'library'
include modulePrefix + 'library-core'
include modulePrefix + 'library-dash'
include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-ui'
include modulePrefix + 'testutils'
include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr'
include modulePrefix + 'extension-ima'
include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/dash')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima')
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
if (gradle.ext.has('exoplayerIncludeCronetExtension')
&& gradle.ext.exoplayerIncludeCronetExtension) {
include modulePrefix + 'extension-cronet'
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
}
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../constants.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
...@@ -45,13 +46,14 @@ android { ...@@ -45,13 +46,14 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile project(':library-dash') compile project(modulePrefix + 'library-dash')
compile project(':library-hls') compile project(modulePrefix + 'library-hls')
compile project(':library-smoothstreaming') compile project(modulePrefix + 'library-smoothstreaming')
compile project(':library-ui') compile project(modulePrefix + 'library-ui')
withExtensionsCompile project(path: ':extension-ffmpeg') withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsCompile project(path: ':extension-flac') withExtensionsCompile project(path: modulePrefix + 'extension-flac')
withExtensionsCompile project(path: ':extension-opus') withExtensionsCompile project(path: modulePrefix + 'extension-ima')
withExtensionsCompile project(path: ':extension-vp9') withExtensionsCompile project(path: modulePrefix + 'extension-opus')
withExtensionsCompile project(path: modulePrefix + 'extension-vp9')
} }
...@@ -16,14 +16,14 @@ ...@@ -16,14 +16,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2404" android:versionCode="2500"
android:versionName="2.4.4"> android:versionName="2.5.0">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/> <uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/> <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24"/> <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="25"/>
<application <application
android:label="@string/application_name" android:label="@string/application_name"
......
...@@ -20,9 +20,9 @@ import android.util.Log; ...@@ -20,9 +20,9 @@ import android.util.Log;
import android.view.Surface; import android.view.Surface;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
...@@ -55,8 +55,8 @@ import java.util.Locale; ...@@ -55,8 +55,8 @@ import java.util.Locale;
/** /**
* Logs player events using {@link Log}. * Logs player events using {@link Log}.
*/ */
/* package */ final class EventLogger implements ExoPlayer.EventListener, /* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener, ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
MetadataRenderer.Output { MetadataRenderer.Output {
...@@ -82,7 +82,7 @@ import java.util.Locale; ...@@ -82,7 +82,7 @@ import java.util.Locale;
startTimeMs = SystemClock.elapsedRealtime(); startTimeMs = SystemClock.elapsedRealtime();
} }
// ExoPlayer.EventListener // Player.EventListener
@Override @Override
public void onLoadingChanged(boolean isLoading) { public void onLoadingChanged(boolean isLoading) {
...@@ -96,6 +96,11 @@ import java.util.Locale; ...@@ -96,6 +96,11 @@ import java.util.Locale;
} }
@Override @Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]");
}
@Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
Log.d(TAG, "positionDiscontinuity"); Log.d(TAG, "positionDiscontinuity");
} }
...@@ -276,7 +281,7 @@ import java.util.Locale; ...@@ -276,7 +281,7 @@ import java.util.Locale;
@Override @Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) { float pixelWidthHeightRatio) {
// Do nothing. Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]");
} }
@Override @Override
...@@ -407,13 +412,13 @@ import java.util.Locale; ...@@ -407,13 +412,13 @@ import java.util.Locale;
private static String getStateString(int state) { private static String getStateString(int state) {
switch (state) { switch (state) {
case ExoPlayer.STATE_BUFFERING: case Player.STATE_BUFFERING:
return "B"; return "B";
case ExoPlayer.STATE_ENDED: case Player.STATE_ENDED:
return "E"; return "E";
case ExoPlayer.STATE_IDLE: case Player.STATE_IDLE:
return "I"; return "I";
case ExoPlayer.STATE_READY: case Player.STATE_READY:
return "R"; return "R";
default: default:
return "?"; return "?";
...@@ -461,4 +466,16 @@ import java.util.Locale; ...@@ -461,4 +466,16 @@ import java.util.Locale;
return enabled ? "[X]" : "[ ]"; 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 "?";
}
}
} }
...@@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity { ...@@ -184,6 +184,7 @@ public class SampleChooserActivity extends Activity {
String[] drmKeyRequestProperties = null; String[] drmKeyRequestProperties = null;
boolean preferExtensionDecoders = false; boolean preferExtensionDecoders = false;
ArrayList<UriSample> playlistSamples = null; ArrayList<UriSample> playlistSamples = null;
String adTagUri = null;
reader.beginObject(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
...@@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity { ...@@ -233,6 +234,9 @@ public class SampleChooserActivity extends Activity {
} }
reader.endArray(); reader.endArray();
break; break;
case "ad_tag_uri":
adTagUri = reader.nextString();
break;
default: default:
throw new ParserException("Unsupported attribute name: " + name); throw new ParserException("Unsupported attribute name: " + name);
} }
...@@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity { ...@@ -246,7 +250,7 @@ public class SampleChooserActivity extends Activity {
preferExtensionDecoders, playlistSamplesArray); preferExtensionDecoders, playlistSamplesArray);
} else { } else {
return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties,
preferExtensionDecoders, uri, extension); preferExtensionDecoders, uri, extension, adTagUri);
} }
} }
...@@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity { ...@@ -402,13 +406,15 @@ public class SampleChooserActivity extends Activity {
public final String uri; public final String uri;
public final String extension; public final String extension;
public final String adTagUri;
public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl,
String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri,
String extension) { String extension, String adTagUri) {
super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders);
this.uri = uri; this.uri = uri;
this.extension = extension; this.extension = extension;
this.adTagUri = adTagUri;
} }
@Override @Override
...@@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity { ...@@ -416,6 +422,7 @@ public class SampleChooserActivity extends Activity {
return super.buildIntent(context) return super.buildIntent(context)
.setData(Uri.parse(uri)) .setData(Uri.parse(uri))
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension) .putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
.setAction(PlayerActivity.ACTION_VIEW); .setAction(PlayerActivity.ACTION_VIEW);
} }
......
...@@ -56,4 +56,6 @@ ...@@ -56,4 +56,6 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string> <string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
</resources> </resources>
# ExoPlayer Cronet Extension # # ExoPlayer Cronet extension #
## Description ## ## 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 [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 [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
## Build Instructions ## ## Build instructions ##
* Checkout ExoPlayer along with Extensions:
``` To use this extension you need to clone the ExoPlayer repository and depend on
git clone https://github.com/google/ExoPlayer.git 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:
* Get the Cronet libraries:
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet` 1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
directory directory
...@@ -27,6 +22,37 @@ git clone https://github.com/google/ExoPlayer.git ...@@ -27,6 +22,37 @@ git clone https://github.com/google/ExoPlayer.git
1. Copy the content of the downloaded `libs` directory into the `jniLibs` 1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension directory of this extension
* In ExoPlayer's `settings.gradle` file, uncomment the Cronet extension * In your `settings.gradle` file, add the following line before the line that
applies `core_settings.gradle`:
```gradle
gradle.ext.exoplayerIncludeCronetExtension = true;
```
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
## Using the extension ##
ExoPlayer requests data through `DataSource` instances. These instances are
either instantiated and injected from application code, or obtained from
instances of `DataSource.Factory` that are instantiated and injected from
application code.
If your application only needs to play http(s) content, using the Cronet
extension is as simple as updating any `DataSource`s and `DataSource.Factory`
instantiations in your application code to use `CronetDataSource` and
`CronetDataSourceFactory` respectively. If your application also needs to play
non-http(s) content such as local files, use
```
new DefaultDataSource(
...
new CronetDataSource(...) /* baseDataSource argument */);
```
and
```
new DefaultDataSourceFactory(
...
new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */);
```
respectively.
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
...@@ -29,11 +30,11 @@ android { ...@@ -29,11 +30,11 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile files('libs/cronet_api.jar') compile files('libs/cronet_api.jar')
compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_common_java.jar')
compile files('libs/cronet_impl_native_java.jar') compile files('libs/cronet_impl_native_java.jar')
androidTestCompile project(':library') androidTestCompile project(modulePrefix + 'library')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
......
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
<instrumentation <instrumentation
android:name="android.test.InstrumentationTestRunner" android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.google.android.exoplayer.ext.cronet" android:targetPackage="com.google.android.exoplayer.ext.cronet"/>
tools:replace="android:targetPackage"/>
</manifest> </manifest>
...@@ -124,6 +124,7 @@ public final class CronetDataSourceTest { ...@@ -124,6 +124,7 @@ public final class CronetDataSourceTest {
when(mockCronetEngine.newUrlRequestBuilder( when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class))) anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
.thenReturn(mockUrlRequestBuilder); .thenReturn(mockUrlRequestBuilder);
when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest); when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
mockStatusResponse(); mockStatusResponse();
...@@ -683,6 +684,15 @@ public final class CronetDataSourceTest { ...@@ -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. // Helper methods.
private void mockStatusResponse() { private void mockStatusResponse() {
......
...@@ -20,6 +20,7 @@ import android.os.ConditionVariable; ...@@ -20,6 +20,7 @@ import android.os.ConditionVariable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; 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.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
...@@ -27,7 +28,6 @@ import com.google.android.exoplayer2.upstream.TransferListener; ...@@ -27,7 +28,6 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Predicate;
import com.google.android.exoplayer2.util.SystemClock;
import java.io.IOException; import java.io.IOException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
...@@ -74,6 +74,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -74,6 +74,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
} }
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.cronet");
}
/** /**
* The default connection timeout, in milliseconds. * The default connection timeout, in milliseconds.
*/ */
...@@ -127,7 +131,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -127,7 +131,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
/** /**
* @param cronetEngine A CronetEngine. * @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 * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from * predicate then an {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
...@@ -141,7 +149,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -141,7 +149,11 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
/** /**
* @param cronetEngine A CronetEngine. * @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 * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from * predicate then an {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
...@@ -156,7 +168,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -156,7 +168,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
RequestProperties defaultRequestProperties) { RequestProperties defaultRequestProperties) {
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties); readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties);
} }
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
...@@ -416,8 +428,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -416,8 +428,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
// Internal methods. // Internal methods.
private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException { private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException {
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(), UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
this, executor); dataSpec.uri.toString(), this, executor).allowDirectExecutor();
// Set the headers. // Set the headers.
boolean isContentTypeHeaderSet = false; boolean isContentTypeHeaderSet = false;
if (defaultRequestProperties != null) { if (defaultRequestProperties != null) {
......
...@@ -16,9 +16,11 @@ ...@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Predicate;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
...@@ -34,43 +36,143 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -34,43 +36,143 @@ public final class CronetDataSourceFactory extends BaseFactory {
*/ */
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS; CronetDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
/** /**
* The default read timeout, in milliseconds. * The default read timeout, in milliseconds.
*/ */
public static final int DEFAULT_READ_TIMEOUT_MILLIS = public static final int DEFAULT_READ_TIMEOUT_MILLIS =
CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS; CronetDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
private final CronetEngine cronetEngine; private final CronetEngineWrapper cronetEngineWrapper;
private final Executor executor; private final Executor executor;
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
private final TransferListener<? super DataSource> transferListener; private final TransferListener<? super DataSource> transferListener;
private final int connectTimeoutMs; private final int connectTimeoutMs;
private final int readTimeoutMs; private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects; private final boolean resetTimeoutOnRedirects;
private final HttpDataSource.Factory fallbackFactory;
/**
* Constructs a CronetDataSourceFactory.
* <p>
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
* fallback {@link HttpDataSource.Factory} will be used instead.
*
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
* cross-protocol redirects.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link CronetDataSource#open}.
* @param transferListener An optional listener.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
* no suitable CronetEngine can be build.
*/
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener,
HttpDataSource.Factory fallbackFactory) {
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
}
/**
* Constructs a CronetDataSourceFactory.
* <p>
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
* {@link DefaultHttpDataSourceFactory} will be used instead.
*
* Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link
* CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables
* cross-protocol redirects.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link CronetDataSource#open}.
* @param transferListener An optional listener.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener, String userAgent) {
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
new DefaultHttpDataSourceFactory(userAgent, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
}
public CronetDataSourceFactory(CronetEngine cronetEngine, /**
* Constructs a CronetDataSourceFactory.
* <p>
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a
* {@link DefaultHttpDataSourceFactory} will be used instead.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate<String> contentTypePredicate, Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener) { TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
this(cronetEngine, executor, contentTypePredicate, transferListener, int readTimeoutMs, boolean resetTimeoutOnRedirects, String userAgent) {
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false); this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
readTimeoutMs, resetTimeoutOnRedirects));
} }
public CronetDataSourceFactory(CronetEngine cronetEngine, /**
* Constructs a CronetDataSourceFactory.
* <p>
* If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
* fallback {@link HttpDataSource.Factory} will be used instead.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then an {@link InvalidContentTypeException} is thrown from
* {@link CronetDataSource#open}.
* @param transferListener An optional listener.
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case
* no suitable CronetEngine can be build.
*/
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper,
Executor executor, Predicate<String> contentTypePredicate, Executor executor, Predicate<String> contentTypePredicate,
TransferListener<? super DataSource> transferListener, int connectTimeoutMs, TransferListener<? super DataSource> transferListener, int connectTimeoutMs,
int readTimeoutMs, boolean resetTimeoutOnRedirects) { int readTimeoutMs, boolean resetTimeoutOnRedirects,
this.cronetEngine = cronetEngine; HttpDataSource.Factory fallbackFactory) {
this.cronetEngineWrapper = cronetEngineWrapper;
this.executor = executor; this.executor = executor;
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
this.transferListener = transferListener; this.transferListener = transferListener;
this.connectTimeoutMs = connectTimeoutMs; this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs; this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
this.fallbackFactory = fallbackFactory;
} }
@Override @Override
protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties protected HttpDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) { defaultRequestProperties) {
CronetEngine cronetEngine = cronetEngineWrapper.getCronetEngine();
if (cronetEngine == null) {
return fallbackFactory.createDataSource();
}
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties); connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, defaultRequestProperties);
} }
......
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cronet;
import android.content.Context;
import android.support.annotation.IntDef;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetProvider;
/**
* A wrapper class for a {@link CronetEngine}.
*/
public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper";
private final CronetEngine cronetEngine;
private final @CronetEngineSource int cronetEngineSource;
/**
* Source of {@link CronetEngine}.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
public @interface CronetEngineSource {}
/**
* Natively bundled Cronet implementation.
*/
public static final int SOURCE_NATIVE = 0;
/**
* Cronet implementation from GMSCore.
*/
public static final int SOURCE_GMS = 1;
/**
* Other (unknown) Cronet implementation.
*/
public static final int SOURCE_UNKNOWN = 2;
/**
* User-provided Cronet engine.
*/
public static final int SOURCE_USER_PROVIDED = 3;
/**
* No Cronet implementation available. Fallback Http provider is used if possible.
*/
public static final int SOURCE_UNAVAILABLE = 4;
/**
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
* {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet
* if both are available.
*
* @param context A context.
*/
public CronetEngineWrapper(Context context) {
this(context, false);
}
/**
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable
* {@link CronetProvider} based on user preference.
*
* @param context A context.
* @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively
* bundled Cronet if both are available.
*/
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) {
CronetEngine cronetEngine = null;
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
List<CronetProvider> cronetProviders = CronetProvider.getAllProviders(context);
// Remove disabled and fallback Cronet providers from list
for (int i = cronetProviders.size() - 1; i >= 0; i--) {
if (!cronetProviders.get(i).isEnabled()
|| CronetProvider.PROVIDER_NAME_FALLBACK.equals(cronetProviders.get(i).getName())) {
cronetProviders.remove(i);
}
}
// Sort remaining providers by type and version.
CronetProviderComparator providerComparator = new CronetProviderComparator(preferGMSCoreCronet);
Collections.sort(cronetProviders, providerComparator);
for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {
String providerName = cronetProviders.get(i).getName();
try {
cronetEngine = cronetProviders.get(i).createBuilder().build();
if (providerComparator.isNativeProvider(providerName)) {
cronetEngineSource = SOURCE_NATIVE;
} else if (providerComparator.isGMSCoreProvider(providerName)) {
cronetEngineSource = SOURCE_GMS;
} else {
cronetEngineSource = SOURCE_UNKNOWN;
}
Log.d(TAG, "CronetEngine built using " + providerName);
} catch (SecurityException e) {
Log.w(TAG, "Failed to build CronetEngine. Please check if current process has "
+ "android.permission.ACCESS_NETWORK_STATE.");
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, "Failed to link Cronet binaries. Please check if native Cronet binaries are "
+ "bundled into your app.");
}
}
if (cronetEngine == null) {
Log.w(TAG, "Cronet not available. Using fallback provider.");
}
this.cronetEngine = cronetEngine;
this.cronetEngineSource = cronetEngineSource;
}
/**
* Creates a wrapper for an existing CronetEngine.
*
* @param cronetEngine An existing CronetEngine.
*/
public CronetEngineWrapper(CronetEngine cronetEngine) {
this.cronetEngine = cronetEngine;
this.cronetEngineSource = SOURCE_USER_PROVIDED;
}
/**
* Returns the source of the wrapped {@link CronetEngine}.
*
* @return A {@link CronetEngineSource} value.
*/
public @CronetEngineSource int getCronetEngineSource() {
return cronetEngineSource;
}
/**
* Returns the wrapped {@link CronetEngine}.
*
* @return The CronetEngine, or null if no CronetEngine is available.
*/
/* package */ CronetEngine getCronetEngine() {
return cronetEngine;
}
private static class CronetProviderComparator implements Comparator<CronetProvider> {
private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet;
public CronetProviderComparator(boolean preferGMSCoreCronet) {
// GMSCore CronetProvider classes are only available in some configurations.
// Thus, we use reflection to copy static name.
String gmsCoreVersionString = null;
try {
Class<?> cronetProviderInstallerClass =
Class.forName("com.google.android.gms.net.CronetProviderInstaller");
Field providerNameField = cronetProviderInstallerClass.getDeclaredField("PROVIDER_NAME");
gmsCoreVersionString = (String) providerNameField.get(null);
} catch (ClassNotFoundException e) {
// GMSCore CronetProvider not available.
} catch (NoSuchFieldException e) {
// GMSCore CronetProvider not available.
} catch (IllegalAccessException e) {
// GMSCore CronetProvider not available.
}
gmsCoreCronetName = gmsCoreVersionString;
this.preferGMSCoreCronet = preferGMSCoreCronet;
}
@Override
public int compare(CronetProvider providerLeft, CronetProvider providerRight) {
int typePreferenceLeft = evaluateCronetProviderType(providerLeft.getName());
int typePreferenceRight = evaluateCronetProviderType(providerRight.getName());
if (typePreferenceLeft != typePreferenceRight) {
return typePreferenceLeft - typePreferenceRight;
}
return -compareVersionStrings(providerLeft.getVersion(), providerRight.getVersion());
}
public boolean isNativeProvider(String providerName) {
return CronetProvider.PROVIDER_NAME_APP_PACKAGED.equals(providerName);
}
public boolean isGMSCoreProvider(String providerName) {
return gmsCoreCronetName != null && gmsCoreCronetName.equals(providerName);
}
/**
* Convert Cronet provider name into a sortable preference value.
* Smaller values are preferred.
*/
private int evaluateCronetProviderType(String providerName) {
if (isNativeProvider(providerName)) {
return 1;
}
if (isGMSCoreProvider(providerName)) {
return preferGMSCoreCronet ? 0 : 2;
}
// Unknown provider type.
return -1;
}
/**
* Compares version strings of format "12.123.35.23".
*/
private static int compareVersionStrings(String versionLeft, String versionRight) {
if (versionLeft == null || versionRight == null) {
return 0;
}
String[] versionStringsLeft = versionLeft.split("\\.");
String[] versionStringsRight = versionRight.split("\\.");
int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length);
for (int i = 0; i < minLength; i++) {
if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
try {
int versionIntLeft = Integer.parseInt(versionStringsLeft[i]);
int versionIntRight = Integer.parseInt(versionStringsRight[i]);
return versionIntLeft - versionIntRight;
} catch (NumberFormatException e) {
return 0;
}
}
}
return 0;
}
}
}
# FfmpegAudioRenderer # # ExoPlayer FFmpeg extension #
## Description ## ## Description ##
...@@ -9,11 +9,10 @@ audio. ...@@ -9,11 +9,10 @@ audio.
## Build instructions ## ## Build instructions ##
* Checkout ExoPlayer along with Extensions To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
``` [top level README][]. In addition, it's necessary to build the extension's
git clone https://github.com/google/ExoPlayer.git native components as follows:
```
* Set the following environment variables: * Set the following environment variables:
...@@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" ...@@ -25,8 +24,6 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
* Download the [Android NDK][] and set its location in an environment variable: * Download the [Android NDK][] and set its location in an environment variable:
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
``` ```
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
``` ```
...@@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ...@@ -106,20 +103,5 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
``` ```
* In your project, you can add a dependency on the extension by using a rule [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
like this: [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
```
// in settings.gradle
include ':..:ExoPlayer:library'
include ':..:ExoPlayer:extension-ffmpeg'
// in build.gradle
dependencies {
compile project(':..:ExoPlayer:library')
compile project(':..:ExoPlayer:extension-ffmpeg')
}
```
* Now, when you build your app, the extension will be built and the native
libraries will be packaged along with the APK.
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
...@@ -30,7 +31,7 @@ android { ...@@ -30,7 +31,7 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
} }
ext { ext {
......
...@@ -30,7 +30,14 @@ import com.google.android.exoplayer2.util.MimeTypes; ...@@ -30,7 +30,14 @@ import com.google.android.exoplayer2.util.MimeTypes;
*/ */
public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
/**
* The number of input and output buffers.
*/
private static final int NUM_BUFFERS = 16; 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 static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6;
private FfmpegDecoder decoder; private FfmpegDecoder decoder;
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.ffmpeg; 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.LibraryLoader;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -23,6 +24,10 @@ import com.google.android.exoplayer2.util.MimeTypes; ...@@ -23,6 +24,10 @@ import com.google.android.exoplayer2.util.MimeTypes;
*/ */
public final class FfmpegLibrary { public final class FfmpegLibrary {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
}
private static final LibraryLoader LOADER = private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg"); new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
...@@ -32,6 +37,8 @@ public final class FfmpegLibrary { ...@@ -32,6 +37,8 @@ public final class FfmpegLibrary {
* Override the names of the FFmpeg native libraries. If an application wishes to call this * 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 * method, it must do so before calling any other method defined by this class, and before
* instantiating a {@link FfmpegAudioRenderer} instance. * instantiating a {@link FfmpegAudioRenderer} instance.
*
* @param libraries The names of the FFmpeg native libraries.
*/ */
public static void setLibraries(String... libraries) { public static void setLibraries(String... libraries) {
LOADER.setLibraries(libraries); LOADER.setLibraries(libraries);
...@@ -53,6 +60,8 @@ public final class FfmpegLibrary { ...@@ -53,6 +60,8 @@ public final class FfmpegLibrary {
/** /**
* Returns whether the underlying library supports the specified MIME type. * Returns whether the underlying library supports the specified MIME type.
*
* @param mimeType The MIME type to check.
*/ */
public static boolean supportsFormat(String mimeType) { public static boolean supportsFormat(String mimeType) {
if (!isAvailable()) { if (!isAvailable()) {
......
# ExoPlayer Flac Extension # # ExoPlayer Flac extension #
## Description ## ## 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 libFLAC (the Flac decoding library) into your app and use it along with
ExoPlayer to play Flac audio on Android devices. ExoPlayer to play Flac audio on Android devices.
[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html
## Build Instructions ## ## Build instructions ##
* Checkout ExoPlayer along with Extensions: To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
``` [top level README][]. In addition, it's necessary to build the extension's
git clone https://github.com/google/ExoPlayer.git native components as follows:
```
* Set the following environment variables: * Set the following environment variables:
...@@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main" ...@@ -26,8 +25,6 @@ FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
* Download the [Android NDK][] and set its location in an environment variable: * Download the [Android NDK][] and set its location in an environment variable:
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
``` ```
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
``` ```
...@@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \ ...@@ -47,20 +44,5 @@ cd "${FLAC_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=all -j4 ${NDK_PATH}/ndk-build APP_ABI=all -j4
``` ```
* In your project, you can add a dependency to the Flac Extension by using a [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
rule like this: [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
```
// in settings.gradle
include ':..:ExoPlayer:library'
include ':..:ExoPlayer:extension-flac'
// in build.gradle
dependencies {
compile project(':..:ExoPlayer:library')
compile project(':..:ExoPlayer:extension-flac')
}
```
* Now, when you build your app, the Flac extension will be built and the native
libraries will be packaged along with the APK.
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
...@@ -30,8 +31,8 @@ android { ...@@ -30,8 +31,8 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
androidTestCompile project(':testutils') androidTestCompile project(modulePrefix + 'testutils')
} }
ext { ext {
......
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
<instrumentation <instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.flac.test" android:targetPackage="com.google.android.exoplayer2.ext.flac.test"
android:name="android.test.InstrumentationTestRunner" android:name="android.test.InstrumentationTestRunner"/>
tools:replace="android:targetPackage"/>
</manifest> </manifest>
...@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac; ...@@ -17,7 +17,8 @@ package com.google.android.exoplayer2.ext.flac;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
/** /**
* Unit test for {@link FlacExtractor}. * Unit test for {@link FlacExtractor}.
...@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; ...@@ -25,7 +26,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public class FlacExtractorTest extends InstrumentationTestCase { public class FlacExtractorTest extends InstrumentationTestCase {
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { ExtractorAsserts.assertBehavior(new ExtractorFactory() {
@Override @Override
public Extractor create() { public Extractor create() {
return new FlacExtractor(); return new FlacExtractor();
......
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; ...@@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
...@@ -57,7 +58,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { ...@@ -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 Context context;
private final Uri uri; private final Uri uri;
...@@ -120,12 +121,17 @@ public class FlacPlaybackTest extends InstrumentationTestCase { ...@@ -120,12 +121,17 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_ENDED if (playbackState == Player.STATE_ENDED
|| (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { || (playbackState == Player.STATE_IDLE && playbackException != null)) {
releasePlayerAndQuitLooper(); releasePlayerAndQuitLooper();
} }
} }
@Override
public void onRepeatModeChanged(int repeatMode) {
// Do nothing.
}
private void releasePlayerAndQuitLooper() { private void releasePlayerAndQuitLooper() {
player.release(); player.release();
Looper.myLooper().quit(); Looper.myLooper().quit();
......
...@@ -159,13 +159,17 @@ public final class FlacExtractor implements Extractor { ...@@ -159,13 +159,17 @@ public final class FlacExtractor implements Extractor {
if (position == 0) { if (position == 0) {
metadataParsed = false; metadataParsed = false;
} }
decoderJni.reset(position); if (decoderJni != null) {
decoderJni.reset(position);
}
} }
@Override @Override
public void release() { public void release() {
decoderJni.release(); if (decoderJni != null) {
decoderJni = null; decoderJni.release();
decoderJni = null;
}
} }
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.LibraryLoader;
/** /**
...@@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader; ...@@ -22,6 +23,10 @@ import com.google.android.exoplayer2.util.LibraryLoader;
*/ */
public final class FlacLibrary { public final class FlacLibrary {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.flac");
}
private static final LibraryLoader LOADER = new LibraryLoader("flacJNI"); private static final LibraryLoader LOADER = new LibraryLoader("flacJNI");
private FlacLibrary() {} private FlacLibrary() {}
...@@ -30,6 +35,8 @@ public final class 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, * 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 * it must do so before calling any other method defined by this class, and before instantiating
* any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances. * any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances.
*
* @param libraries The names of the Flac native libraries.
*/ */
public static void setLibraries(String... libraries) { public static void setLibraries(String... libraries) {
LOADER.setLibraries(libraries); LOADER.setLibraries(libraries);
......
# ExoPlayer GVR Extension # # ExoPlayer GVR extension #
## Description ## ## Description ##
...@@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a ...@@ -6,7 +6,10 @@ The GVR extension wraps the [Google VR SDK for Android][]. It provides a
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields. of surround sound and ambisonic soundfields.
## Using the extension ## [Google VR SDK for Android]: https://developers.google.com/vr/android/
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
## Getting the extension ##
The easiest way to use the extension is to add it as a gradle dependency. You The easiest way to use the extension is to add it as a gradle dependency. You
need to make sure you have the jcenter repository included in the `build.gradle` need to make sure you have the jcenter repository included in the `build.gradle`
...@@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' ...@@ -27,12 +30,15 @@ compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
where `rX.X.X` is the version, which must match the version of the ExoPlayer where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used. library being used.
## Using GvrAudioProcessor ## Alternatively, you can clone the ExoPlayer repository and depend on the module
locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
## Using the extension ##
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to * If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
return a GvrAudioProcessor. return a GvrAudioProcessor.
* If constructing renderers directly, pass a GvrAudioProcessor to * If constructing renderers directly, pass a GvrAudioProcessor to
MediaCodecAudioRenderer's constructor. MediaCodecAudioRenderer's constructor.
[Google VR SDK for Android]: https://developers.google.com/vr/android/ [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
...@@ -24,7 +25,7 @@ android { ...@@ -24,7 +25,7 @@ android {
} }
dependencies { dependencies {
compile project(':library-core') compile project(modulePrefix + 'library-core')
compile 'com.google.vr:sdk-audio:1.60.1' compile 'com.google.vr:sdk-audio:1.60.1'
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.gvr; package com.google.android.exoplayer2.ext.gvr;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.vr.sdk.audio.GvrAudioSurround; import com.google.vr.sdk.audio.GvrAudioSurround;
...@@ -28,6 +29,10 @@ import java.nio.ByteOrder; ...@@ -28,6 +29,10 @@ import java.nio.ByteOrder;
*/ */
public final class GvrAudioProcessor implements AudioProcessor { 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 FRAMES_PER_OUTPUT_BUFFER = 1024;
private static final int OUTPUT_CHANNEL_COUNT = 2; private static final int OUTPUT_CHANNEL_COUNT = 2;
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output. 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 { ...@@ -56,6 +61,11 @@ public final class GvrAudioProcessor implements AudioProcessor {
/** /**
* Updates the listener head orientation. May be called on any thread. See * Updates the listener head orientation. May be called on any thread. See
* {@code GvrAudioSurround.updateNativeOrientation}. * {@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) { public synchronized void updateOrientation(float w, float x, float y, float z) {
this.w = w; this.w = w;
......
# 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
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'
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.ext.ima">
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
</manifest>
/*
* 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;
}
}
/*
* 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);
}
}
# 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
// 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'
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.mediasession"/>
/*
* Copyright (c) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.mediasession;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
/**
* A default implementation of {@link MediaSessionConnector.PlaybackController}.
* <p>
* Methods can be safely overridden by subclasses to intercept calls for given actions.
*/
public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController {
/**
* The default fast forward increment, in milliseconds.
*/
public static final int DEFAULT_FAST_FORWARD_MS = 15000;
/**
* The default rewind increment, in milliseconds.
*/
public static final int DEFAULT_REWIND_MS = 5000;
private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_STOP;
protected final long rewindIncrementMs;
protected final long fastForwardIncrementMs;
/**
* Creates a new instance.
* <p>
* Equivalent to {@code DefaultPlaybackController(
* DefaultPlaybackController.DEFAULT_REWIND_MS,
* DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}.
*/
public DefaultPlaybackController() {
this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS);
}
/**
* Creates a new instance with the given fast forward and rewind increments.
*
* @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will
* cause the rewind action to be disabled.
* @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative
* value will cause the fast forward action to be removed.
*/
public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) {
this.rewindIncrementMs = rewindIncrementMs;
this.fastForwardIncrementMs = fastForwardIncrementMs;
}
@Override
public long getSupportedPlaybackActions(Player player) {
if (player == null || player.getCurrentTimeline().isEmpty()) {
return 0;
}
long actions = BASE_ACTIONS;
if (player.isCurrentWindowSeekable()) {
actions |= PlaybackStateCompat.ACTION_SEEK_TO;
}
if (fastForwardIncrementMs > 0) {
actions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
}
if (rewindIncrementMs > 0) {
actions |= PlaybackStateCompat.ACTION_REWIND;
}
return actions;
}
@Override
public void onPlay(Player player) {
player.setPlayWhenReady(true);
}
@Override
public void onPause(Player player) {
player.setPlayWhenReady(false);
}
@Override
public void onSeekTo(Player player, long position) {
long duration = player.getDuration();
if (duration != C.TIME_UNSET) {
position = Math.min(position, duration);
}
player.seekTo(Math.max(position, 0));
}
@Override
public void onFastForward(Player player) {
if (fastForwardIncrementMs <= 0) {
return;
}
onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs);
}
@Override
public void onRewind(Player player) {
if (rewindIncrementMs <= 0) {
return;
}
onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs);
}
@Override
public void onStop(Player player) {
player.stop();
}
}
package com.google.android.exoplayer2.ext.mediasession;
/*
* Copyright (c) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context;
import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.util.RepeatModeUtil;
/**
* Provides a custom action for toggling repeat modes.
*/
public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider {
/**
* The default repeat toggle modes.
*/
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL;
private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE";
private final Player player;
@RepeatModeUtil.RepeatToggleModes
private final int repeatToggleModes;
private final CharSequence repeatAllDescription;
private final CharSequence repeatOneDescription;
private final CharSequence repeatOffDescription;
/**
* Creates a new instance.
* <p>
* Equivalent to {@code RepeatModeActionProvider(context, player,
* RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}.
*
* @param context The context.
* @param player The player on which to toggle the repeat mode.
*/
public RepeatModeActionProvider(Context context, Player player) {
this(context, player, DEFAULT_REPEAT_TOGGLE_MODES);
}
/**
* Creates a new instance enabling the given repeat toggle modes.
*
* @param context The context.
* @param player The player on which to toggle the repeat mode.
* @param repeatToggleModes The toggle modes to enable.
*/
public RepeatModeActionProvider(Context context, Player player,
@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.player = player;
this.repeatToggleModes = repeatToggleModes;
repeatAllDescription = context.getString(R.string.exo_media_action_repeat_all_description);
repeatOneDescription = context.getString(R.string.exo_media_action_repeat_one_description);
repeatOffDescription = context.getString(R.string.exo_media_action_repeat_off_description);
}
@Override
public void onCustomAction(String action, Bundle extras) {
int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) {
player.setRepeatMode(proposedMode);
}
}
@Override
public PlaybackStateCompat.CustomAction getCustomAction() {
CharSequence actionLabel;
int iconResourceId;
switch (player.getRepeatMode()) {
case Player.REPEAT_MODE_ONE:
actionLabel = repeatOneDescription;
iconResourceId = R.drawable.exo_media_action_repeat_one;
break;
case Player.REPEAT_MODE_ALL:
actionLabel = repeatAllDescription;
iconResourceId = R.drawable.exo_media_action_repeat_all;
break;
case Player.REPEAT_MODE_OFF:
default:
actionLabel = repeatOffDescription;
iconResourceId = R.drawable.exo_media_action_repeat_off;
break;
}
PlaybackStateCompat.CustomAction.Builder repeatBuilder = new PlaybackStateCompat.CustomAction
.Builder(ACTION_REPEAT_MODE, actionLabel, iconResourceId);
return repeatBuilder.build();
}
}
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.mediasession;
import android.support.annotation.Nullable;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the
* windows of a {@link Player}'s {@link Timeline} to the media session queue.
*/
public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator {
public static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;
public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
private final MediaSessionCompat mediaSession;
protected final int maxQueueSize;
private long activeQueueItemId;
/**
* Creates an instance for a given {@link MediaSessionCompat}.
* <p>
* Equivalent to {@code TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}.
*
* @param mediaSession The {@link MediaSessionCompat}.
*/
public TimelineQueueNavigator(MediaSessionCompat mediaSession) {
this(mediaSession, DEFAULT_MAX_QUEUE_SIZE);
}
/**
* Creates an instance for a given {@link MediaSessionCompat} and maximum queue size.
* <p>
* If the number of windows in the {@link Player}'s {@link Timeline} exceeds {@code maxQueueSize},
* the media session queue will correspond to {@code maxQueueSize} windows centered on the one
* currently being played.
*
* @param mediaSession The {@link MediaSessionCompat}.
* @param maxQueueSize The maximum queue size.
*/
public TimelineQueueNavigator(MediaSessionCompat mediaSession, int maxQueueSize) {
this.mediaSession = mediaSession;
this.maxQueueSize = maxQueueSize;
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
}
/**
* Gets the {@link MediaDescriptionCompat} for a given timeline window index.
*
* @param windowIndex The timeline window index for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
*/
public abstract MediaDescriptionCompat getMediaDescription(int windowIndex);
@Override
public long getSupportedQueueNavigatorActions(Player player) {
if (player == null || player.getCurrentTimeline().getWindowCount() < 2) {
return 0;
}
if (player.getRepeatMode() != Player.REPEAT_MODE_OFF) {
return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
}
int currentWindowIndex = player.getCurrentWindowIndex();
long actions;
if (currentWindowIndex == 0) {
actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
} else if (currentWindowIndex == player.getCurrentTimeline().getWindowCount() - 1) {
actions = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
} else {
actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
}
return actions | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
}
@Override
public final void onTimelineChanged(Player player) {
publishFloatingQueueWindow(player);
}
@Override
public final void onCurrentWindowIndexChanged(Player player) {
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
publishFloatingQueueWindow(player);
} else if (!player.getCurrentTimeline().isEmpty()) {
activeQueueItemId = player.getCurrentWindowIndex();
}
}
@Override
public final long getActiveQueueItemId(@Nullable Player player) {
return activeQueueItemId;
}
@Override
public void onSkipToPrevious(Player player) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(),
player.getRepeatMode());
if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS
|| previousWindowIndex == C.INDEX_UNSET) {
player.seekTo(0);
} else {
player.seekTo(previousWindowIndex, C.TIME_UNSET);
}
}
@Override
public void onSkipToQueueItem(Player player, long id) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int windowIndex = (int) id;
if (0 <= windowIndex && windowIndex < timeline.getWindowCount()) {
player.seekTo(windowIndex, C.TIME_UNSET);
}
}
@Override
public void onSkipToNext(Player player) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return;
}
int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(),
player.getRepeatMode());
if (nextWindowIndex != C.INDEX_UNSET) {
player.seekTo(nextWindowIndex, C.TIME_UNSET);
}
}
@Override
public void onSetShuffleModeEnabled(Player player, boolean enabled) {
// TODO: Implement this.
}
private void publishFloatingQueueWindow(Player player) {
if (player.getCurrentTimeline().isEmpty()) {
mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
int windowCount = player.getCurrentTimeline().getWindowCount();
int currentWindowIndex = player.getCurrentWindowIndex();
int queueSize = Math.min(maxQueueSize, windowCount);
int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
windowCount - queueSize);
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) {
queue.add(new MediaSessionCompat.QueueItem(getMediaDescription(i), i));
}
mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex;
}
}
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#4EFFFFFF"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="32dp"
android:width="32dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z"/>
</vector>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Herhaal alles"</string>
<string name="exo_media_action_repeat_off_description">"Herhaal niks"</string>
<string name="exo_media_action_repeat_one_description">"Herhaal een"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"ሁሉንም ድገም"</string>
<string name="exo_media_action_repeat_off_description">"ምንም አትድገም"</string>
<string name="exo_media_action_repeat_one_description">"አንዱን ድገም"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"تكرار الكل"</string>
<string name="exo_media_action_repeat_off_description">"عدم التكرار"</string>
<string name="exo_media_action_repeat_one_description">"تكرار مقطع واحد"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Bütün təkrarlayın"</string>
<string name="exo_media_action_repeat_one_description">"Təkrar bir"</string>
<string name="exo_media_action_repeat_off_description">"Heç bir təkrar"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string>
<string name="exo_media_action_repeat_off_description">"Ne ponavljaj nijednu"</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jednu"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Паўтарыць усё"</string>
<string name="exo_media_action_repeat_off_description">"Паўтараць ні"</string>
<string name="exo_media_action_repeat_one_description">"Паўтарыць адзін"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Повтаряне на всички"</string>
<string name="exo_media_action_repeat_off_description">"Без повтаряне"</string>
<string name="exo_media_action_repeat_one_description">"Повтаряне на един елемент"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"সবগুলির পুনরাবৃত্তি করুন"</string>
<string name="exo_media_action_repeat_off_description">"একটিরও পুনরাবৃত্তি করবেন না"</string>
<string name="exo_media_action_repeat_one_description">"একটির পুনরাবৃত্তি করুন"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovite sve"</string>
<string name="exo_media_action_repeat_off_description">"Ne ponavljaju"</string>
<string name="exo_media_action_repeat_one_description">"Ponovite jedan"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeteix-ho tot"</string>
<string name="exo_media_action_repeat_off_description">"No en repeteixis cap"</string>
<string name="exo_media_action_repeat_one_description">"Repeteix-ne un"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Opakovat vše"</string>
<string name="exo_media_action_repeat_off_description">"Neopakovat"</string>
<string name="exo_media_action_repeat_one_description">"Opakovat jednu položku"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Gentag alle"</string>
<string name="exo_media_action_repeat_off_description">"Gentag ingen"</string>
<string name="exo_media_action_repeat_one_description">"Gentag en"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Alle wiederholen"</string>
<string name="exo_media_action_repeat_off_description">"Keinen Titel wiederholen"</string>
<string name="exo_media_action_repeat_one_description">"Einen Titel wiederholen"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Επανάληψη όλων"</string>
<string name="exo_media_action_repeat_off_description">"Καμία επανάληψη"</string>
<string name="exo_media_action_repeat_one_description">"Επανάληψη ενός στοιχείου"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repeat all"</string>
<string name="exo_media_action_repeat_off_description">"Repeat none"</string>
<string name="exo_media_action_repeat_one_description">"Repeat one"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"No repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir uno"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Korda kõike"</string>
<string name="exo_media_action_repeat_off_description">"Ära korda midagi"</string>
<string name="exo_media_action_repeat_one_description">"Korda ühte"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Errepikatu guztiak"</string>
<string name="exo_media_action_repeat_off_description">"Ez errepikatu"</string>
<string name="exo_media_action_repeat_one_description">"Errepikatu bat"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"تکرار همه"</string>
<string name="exo_media_action_repeat_off_description">"تکرار هیچ‌کدام"</string>
<string name="exo_media_action_repeat_one_description">"یک‌بار تکرار"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Toista kaikki"</string>
<string name="exo_media_action_repeat_off_description">"Toista ei mitään"</string>
<string name="exo_media_action_repeat_one_description">"Toista yksi"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_media_action_repeat_off_description">"Aucune répétition"</string>
<string name="exo_media_action_repeat_one_description">"Répéter un élément"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_media_action_repeat_off_description">"Ne rien lire en boucle"</string>
<string name="exo_media_action_repeat_one_description">"Lire en boucle un élément"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Repetir todo"</string>
<string name="exo_media_action_repeat_off_description">"Non repetir"</string>
<string name="exo_media_action_repeat_one_description">"Repetir un"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"બધા પુનરાવર્તન કરો"</string>
<string name="exo_media_action_repeat_off_description">"કંઈ પુનરાવર્તન કરો"</string>
<string name="exo_media_action_repeat_one_description">"એક પુનરાવર્તન કરો"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"सभी को दोहराएं"</string>
<string name="exo_media_action_repeat_off_description">"कुछ भी न दोहराएं"</string>
<string name="exo_media_action_repeat_one_description">"एक दोहराएं"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ponovi sve"</string>
<string name="exo_media_action_repeat_off_description">"Bez ponavljanja"</string>
<string name="exo_media_action_repeat_one_description">"Ponovi jedno"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Összes ismétlése"</string>
<string name="exo_media_action_repeat_off_description">"Nincs ismétlés"</string>
<string name="exo_media_action_repeat_one_description">"Egy ismétlése"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"կրկնել այն ամենը"</string>
<string name="exo_media_action_repeat_off_description">"Չկրկնել"</string>
<string name="exo_media_action_repeat_one_description">"Կրկնել մեկը"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ulangi Semua"</string>
<string name="exo_media_action_repeat_off_description">"Jangan Ulangi"</string>
<string name="exo_media_action_repeat_one_description">"Ulangi Satu"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Endurtaka allt"</string>
<string name="exo_media_action_repeat_off_description">"Endurtaka ekkert"</string>
<string name="exo_media_action_repeat_one_description">"Endurtaka eitt"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"Ripeti tutti"</string>
<string name="exo_media_action_repeat_off_description">"Non ripetere nessuno"</string>
<string name="exo_media_action_repeat_one_description">"Ripeti uno"</string>
</resources>
<?xml version="1.0"?>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="exo_media_action_repeat_all_description">"חזור על הכל"</string>
<string name="exo_media_action_repeat_off_description">"אל תחזור על כלום"</string>
<string name="exo_media_action_repeat_one_description">"חזור על פריט אחד"</string>
</resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment