Commit 8bed0089 by Will

Merge remote-tracking branch 'upstream/dev-v2' into dev-v2

parents 99960ace f56db87c
Showing with 1452 additions and 682 deletions

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

...@@ -65,6 +65,7 @@ extensions/vp9/src/main/jni/libyuv ...@@ -65,6 +65,7 @@ extensions/vp9/src/main/jni/libyuv
# AV1 extension # AV1 extension
extensions/av1/src/main/jni/libgav1 extensions/av1/src/main/jni/libgav1
extensions/av1/src/main/jni/cpu_features
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
......
# ExoPlayer # # ExoPlayer <img src="https://img.shields.io/github/v/release/google/ExoPlayer.svg?label=latest"/> #
ExoPlayer is an application level media player for Android. It provides an ExoPlayer is an application level media player for Android. It provides an
alternative to Android’s MediaPlayer API for playing audio and video both alternative to Android’s MediaPlayer API for playing audio and video both
......
...@@ -17,7 +17,7 @@ buildscript { ...@@ -17,7 +17,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.3' classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.novoda:bintray-release:0.9.1' classpath 'com.novoda:bintray-release:0.9.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
} }
...@@ -26,6 +26,7 @@ allprojects { ...@@ -26,6 +26,7 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
} }
project.ext { project.ext {
exoplayerPublishEnabled = false exoplayerPublishEnabled = false
......
// Copyright (C) 2020 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: "$gradle.ext.exoplayerSettingsDir/constants.gradle"
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions.unitTests.includeAndroidResources = true
}
...@@ -13,24 +13,28 @@ ...@@ -13,24 +13,28 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.11.4' releaseVersion = '2.12.0'
releaseVersionCode = 2011004 releaseVersionCode = 2012000
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
compileSdkVersion = 29 compileSdkVersion = 29
dexmakerVersion = '2.21.0' dexmakerVersion = '2.21.0'
junitVersion = '4.13-rc-2' junitVersion = '4.13-rc-2'
guavaVersion = '28.2-android' guavaVersion = '27.1-android'
mockitoVersion = '2.25.0' mockitoVersion = '2.28.2'
robolectricVersion = '4.3.1' mockWebServerVersion = '3.12.0'
checkerframeworkVersion = '2.5.0' robolectricVersion = '4.4'
checkerframeworkVersion = '3.3.0'
checkerframeworkCompatVersion = '2.5.0'
jsr305Version = '3.0.2' jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.3.70' kotlinAnnotationsVersion = '1.3.70'
androidxAnnotationVersion = '1.1.0' androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0' androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0' androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1' androidxMediaVersion = '1.0.1'
androidxMultidexVersion = '2.0.0'
androidxRecyclerViewVersion = '1.1.0'
androidxTestCoreVersion = '1.2.0' androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1' androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0' androidxTestRunnerVersion = '1.2.0'
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
// 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.
def rootDir = gradle.ext.exoplayerRoot def rootDir = gradle.ext.exoplayerRoot
if (!gradle.ext.has('exoplayerSettingsDir')) {
gradle.ext.exoplayerSettingsDir =
new File(rootDir.toString()).getCanonicalPath()
}
def modulePrefix = ':' def modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
...@@ -35,6 +39,7 @@ include modulePrefix + 'extension-ima' ...@@ -35,6 +39,7 @@ include modulePrefix + 'extension-ima'
include modulePrefix + 'extension-cast' include modulePrefix + 'extension-cast'
include modulePrefix + 'extension-cronet' include modulePrefix + 'extension-cronet'
include modulePrefix + 'extension-mediasession' include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-media2'
include modulePrefix + 'extension-okhttp' include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus' include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-vp9'
...@@ -61,6 +66,7 @@ project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensio ...@@ -61,6 +66,7 @@ project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensio
project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast')
project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet') project(modulePrefix + 'extension-cronet').projectDir = new File(rootDir, 'extensions/cronet')
project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession')
project(modulePrefix + 'extension-media2').projectDir = new File(rootDir, 'extensions/media2')
project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp')
project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus')
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
......
...@@ -2,3 +2,24 @@ ...@@ -2,3 +2,24 @@
This directory contains applications that demonstrate how to use ExoPlayer. This directory contains applications that demonstrate how to use ExoPlayer.
Browse the individual demos and their READMEs to learn more. Browse the individual demos and their READMEs to learn more.
## Running a demo ##
### From Android Studio ###
* File -> New -> Import Project -> Specify the root ExoPlayer folder.
* Choose the demo from the run configuration dropdown list.
* Click Run.
### Using gradle from the command line: ###
* Open a Terminal window at the root ExoPlayer folder.
* Run `./gradlew projects` to show all projects. Demo projects start with `demo`.
* Run `./gradlew :<demo name>:tasks` to view the list of available tasks for
the demo project. Choose an install option from the `Install tasks` section.
* Run `./gradlew :<demo name>:<install task>`.
**Example**:
`./gradlew :demo:installNoExtensionsDebug` installs the main ExoPlayer demo app
in debug mode with no extensions.
...@@ -2,3 +2,6 @@ ...@@ -2,3 +2,6 @@
This folder contains a demo application that showcases ExoPlayer integration This folder contains a demo application that showcases ExoPlayer integration
with Google Cast. with Google Cast.
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.
...@@ -27,6 +27,7 @@ android { ...@@ -27,6 +27,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
...@@ -57,8 +58,9 @@ dependencies { ...@@ -57,8 +58,9 @@ dependencies {
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast') implementation project(modulePrefix + 'extension-cast')
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.2.1'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
/*
* Copyright 2020 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.castdemo;
import androidx.multidex.MultiDexApplication;
// Note: Multidex is enabled in code not AndroidManifest.xml because the internal build system
// doesn't dejetify MultiDexApplication in AndroidManifest.xml.
/** Application for multidex support. */
public final class DemoApplication extends MultiDexApplication {}
...@@ -29,7 +29,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer; ...@@ -29,7 +29,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
...@@ -61,7 +60,6 @@ import java.util.ArrayList; ...@@ -61,7 +60,6 @@ import java.util.ArrayList;
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT); new DefaultHttpDataSourceFactory(USER_AGENT);
private final DefaultMediaSourceFactory defaultMediaSourceFactory;
private final PlayerView localPlayerView; private final PlayerView localPlayerView;
private final PlayerControlView castControlView; private final PlayerControlView castControlView;
private final DefaultTrackSelector trackSelector; private final DefaultTrackSelector trackSelector;
...@@ -97,7 +95,6 @@ import java.util.ArrayList; ...@@ -97,7 +95,6 @@ import java.util.ArrayList;
trackSelector = new DefaultTrackSelector(context); trackSelector = new DefaultTrackSelector(context);
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build(); exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
defaultMediaSourceFactory = DefaultMediaSourceFactory.newInstance(context, DATA_SOURCE_FACTORY);
exoPlayer.addListener(this); exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer); localPlayerView.setPlayer(exoPlayer);
......
/*
* Copyright (C) 2020 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.
*/
@NonNullApi
package com.google.android.exoplayer2.castdemo;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -8,4 +8,7 @@ drawn using an Android canvas, and includes the current frame's presentation ...@@ -8,4 +8,7 @@ drawn using an Android canvas, and includes the current frame's presentation
timestamp, to show how to get the timestamp of the frame currently in the timestamp, to show how to get the timestamp of the frame currently in the
off-screen surface texture. off-screen surface texture.
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.
[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView [GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView
...@@ -49,5 +49,5 @@ dependencies { ...@@ -49,5 +49,5 @@ dependencies {
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
} }
...@@ -32,4 +32,3 @@ void main() { ...@@ -32,4 +32,3 @@ void main() {
gl_FragColor = videoColor * (1.0 - overlayColor.a) gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a; + overlayColor * overlayColor.a;
} }
...@@ -11,11 +11,10 @@ ...@@ -11,11 +11,10 @@
// 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.
attribute vec4 a_position; attribute vec2 a_position;
attribute vec3 a_texcoord; attribute vec2 a_texcoord;
varying vec2 v_texcoord; varying vec2 v_texcoord;
void main() { void main() {
gl_Position = a_position; gl_Position = vec4(a_position.x, a_position.y, 0, 1);
v_texcoord = a_texcoord.xy; v_texcoord = a_texcoord;
} }
...@@ -88,18 +88,9 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -88,18 +88,9 @@ import javax.microedition.khronos.opengles.GL10;
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program); GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
for (GlUtil.Attribute attribute : attributes) { for (GlUtil.Attribute attribute : attributes) {
if (attribute.name.equals("a_position")) { if (attribute.name.equals("a_position")) {
attribute.setBuffer( attribute.setBuffer(new float[] {-1, -1, 1, -1, -1, 1, 1, 1}, 2);
new float[] {
-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
1.0f, 0.0f, 1.0f,
},
4);
} else if (attribute.name.equals("a_texcoord")) { } else if (attribute.name.equals("a_texcoord")) {
attribute.setBuffer( attribute.setBuffer(new float[] {0, 1, 1, 1, 0, 0, 1, 0}, 2);
new float[] {
0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,
},
3);
} }
} }
this.attributes = attributes; this.attributes = attributes;
......
...@@ -24,6 +24,7 @@ import android.widget.FrameLayout; ...@@ -24,6 +24,7 @@ import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
...@@ -138,13 +139,12 @@ public final class MainActivity extends Activity { ...@@ -138,13 +139,12 @@ public final class MainActivity extends Activity {
ACTION_VIEW.equals(action) ACTION_VIEW.equals(action)
? Assertions.checkNotNull(intent.getData()) ? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI); : Uri.parse(DEFAULT_MEDIA_URI);
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
DrmSessionManager drmSessionManager; DrmSessionManager drmSessionManager;
if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) { if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
HttpMediaDrmCallback drmCallback = HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager = drmSessionManager =
...@@ -155,21 +155,19 @@ public final class MainActivity extends Activity { ...@@ -155,21 +155,19 @@ public final class MainActivity extends Activity {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
} }
DataSource.Factory dataSourceFactory = DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
new DefaultDataSourceFactory(
this, Util.getUserAgent(this, getString(R.string.application_name)));
MediaSource mediaSource; MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) { if (type == C.TYPE_DASH) {
mediaSource = mediaSource =
new DashMediaSource.Factory(dataSourceFactory) new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager) .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri); .createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) { } else if (type == C.TYPE_OTHER) {
mediaSource = mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory) new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager) .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri); .createMediaSource(MediaItem.fromUri(uri));
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
......
/*
* Copyright (C) 2020 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.
*/
@NonNullApi
package com.google.android.exoplayer2.gldemo;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -27,4 +27,3 @@ ...@@ -27,4 +27,3 @@
app:surface_type="none"/> app:surface_type="none"/>
</FrameLayout> </FrameLayout>
...@@ -3,3 +3,6 @@ ...@@ -3,3 +3,6 @@
This is the main ExoPlayer demo application. It uses ExoPlayer to play a number This is the main ExoPlayer demo application. It uses ExoPlayer to play a number
of test streams. It can be used as a starting point or reference project when of test streams. It can be used as a starting point or reference project when
developing other applications that make use of the ExoPlayer library. developing other applications that make use of the ExoPlayer library.
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.
...@@ -27,6 +27,7 @@ android { ...@@ -27,6 +27,7 @@ android {
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
} }
buildTypes { buildTypes {
...@@ -49,34 +50,46 @@ android { ...@@ -49,34 +50,46 @@ android {
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities' disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
} }
flavorDimensions "extensions" flavorDimensions "decoderExtensions"
productFlavors { productFlavors {
noExtensions { noDecoderExtensions {
dimension "extensions" dimension "decoderExtensions"
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "false"
} }
withExtensions { withDecoderExtensions {
dimension "extensions" dimension "decoderExtensions"
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "true"
} }
} }
} }
dependencies { dependencies {
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:1.2.1'
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
withExtensionsImplementation project(path: modulePrefix + 'extension-av1') implementation project(modulePrefix + 'extension-cronet')
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') implementation project(modulePrefix + 'extension-ima')
withExtensionsImplementation project(path: modulePrefix + 'extension-flac') withDecoderExtensionsImplementation project(modulePrefix + 'extension-av1')
withExtensionsImplementation project(path: modulePrefix + 'extension-ima') withDecoderExtensionsImplementation project(modulePrefix + 'extension-ffmpeg')
withExtensionsImplementation project(path: modulePrefix + 'extension-opus') withDecoderExtensionsImplementation project(modulePrefix + 'extension-flac')
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9') withDecoderExtensionsImplementation project(modulePrefix + 'extension-opus')
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp') withDecoderExtensionsImplementation project(modulePrefix + 'extension-vp9')
withDecoderExtensionsImplementation project(modulePrefix + 'extension-rtmp')
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
# Proguard rules specific to the main demo app. # Proguard rules specific to the main demo app.
# Constructor accessed via reflection in PlayerActivity
-dontnote com.google.android.exoplayer2.ext.ima.ImaAdsLoader
-keepclassmembers class com.google.android.exoplayer2.ext.ima.ImaAdsLoader {
<init>(android.content.Context, android.net.Uri);
}
...@@ -35,8 +35,8 @@ ...@@ -35,8 +35,8 @@
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:name="com.google.android.exoplayer2.demo.DemoApplication" android:name="androidx.multidex.MultiDexApplication"
tools:ignore="UnusedAttribute"> tools:targetApi="29">
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity" <activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
......
...@@ -15,11 +15,12 @@ ...@@ -15,11 +15,12 @@
*/ */
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import static com.google.android.exoplayer2.demo.DemoApplication.DOWNLOAD_NOTIFICATION_CHANNEL_ID; import static com.google.android.exoplayer2.demo.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID;
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
...@@ -49,10 +50,9 @@ public class DemoDownloadService extends DownloadService { ...@@ -49,10 +50,9 @@ public class DemoDownloadService extends DownloadService {
protected DownloadManager getDownloadManager() { protected DownloadManager getDownloadManager() {
// This will only happen once, because getDownloadManager is guaranteed to be called only once // This will only happen once, because getDownloadManager is guaranteed to be called only once
// in the life cycle of the process. // in the life cycle of the process.
DemoApplication application = (DemoApplication) getApplication(); DownloadManager downloadManager = DemoUtil.getDownloadManager(/* context= */ this);
DownloadManager downloadManager = application.getDownloadManager();
DownloadNotificationHelper downloadNotificationHelper = DownloadNotificationHelper downloadNotificationHelper =
application.getDownloadNotificationHelper(); DemoUtil.getDownloadNotificationHelper(/* context= */ this);
downloadManager.addListener( downloadManager.addListener(
new TerminalStateNotificationHelper( new TerminalStateNotificationHelper(
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1)); this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
...@@ -67,10 +67,13 @@ public class DemoDownloadService extends DownloadService { ...@@ -67,10 +67,13 @@ public class DemoDownloadService extends DownloadService {
@Override @Override
@NonNull @NonNull
protected Notification getForegroundNotification(@NonNull List<Download> downloads) { protected Notification getForegroundNotification(@NonNull List<Download> downloads) {
return ((DemoApplication) getApplication()) return DemoUtil.getDownloadNotificationHelper(/* context= */ this)
.getDownloadNotificationHelper()
.buildProgressNotification( .buildProgressNotification(
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads); /* context= */ this,
R.drawable.ic_download,
/* contentIntent= */ null,
/* message= */ null,
downloads);
} }
/** /**
...@@ -94,17 +97,20 @@ public class DemoDownloadService extends DownloadService { ...@@ -94,17 +97,20 @@ public class DemoDownloadService extends DownloadService {
} }
@Override @Override
public void onDownloadChanged(@NonNull DownloadManager manager, @NonNull Download download) { public void onDownloadChanged(
DownloadManager downloadManager, Download download, @Nullable Exception finalException) {
Notification notification; Notification notification;
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
notification = notification =
notificationHelper.buildDownloadCompletedNotification( notificationHelper.buildDownloadCompletedNotification(
context,
R.drawable.ic_download_done, R.drawable.ic_download_done,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data)); Util.fromUtf8Bytes(download.request.data));
} else if (download.state == Download.STATE_FAILED) { } else if (download.state == Download.STATE_FAILED) {
notification = notification =
notificationHelper.buildDownloadFailedNotification( notificationHelper.buildDownloadFailedNotification(
context,
R.drawable.ic_download_done, R.drawable.ic_download_done,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data)); Util.fromUtf8Bytes(download.request.data));
......
...@@ -286,7 +286,7 @@ public final class TrackSelectionDialog extends DialogFragment { ...@@ -286,7 +286,7 @@ public final class TrackSelectionDialog extends DialogFragment {
private final class FragmentAdapter extends FragmentPagerAdapter { private final class FragmentAdapter extends FragmentPagerAdapter {
public FragmentAdapter(FragmentManager fragmentManager) { public FragmentAdapter(FragmentManager fragmentManager) {
super(fragmentManager); super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
} }
@Override @Override
......
/*
* Copyright (C) 2020 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.
*/
@NonNullApi
package com.google.android.exoplayer2.demo;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -15,14 +15,17 @@ ...@@ -15,14 +15,17 @@
--> -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root" android:id="@+id/root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"> android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view" <com.google.android.exoplayer2.ui.StyledPlayerView android:id="@+id/player_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"
app:show_shuffle_button="true"
app:show_subtitle_button="true"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
......
...@@ -19,12 +19,4 @@ ...@@ -19,12 +19,4 @@
android:title="@string/prefer_extension_decoders" android:title="@string/prefer_extension_decoders"
android:checkable="true" android:checkable="true"
app:showAsAction="never"/> app:showAsAction="never"/>
<item android:id="@+id/random_abr"
android:title="@string/random_abr"
android:checkable="true"
app:showAsAction="never"/>
<item android:id="@+id/tunneling"
android:title="@string/tunneling"
android:checkable="true"
app:showAsAction="never"/>
</menu> </menu>
...@@ -25,16 +25,10 @@ ...@@ -25,16 +25,10 @@
<string name="error_generic">Playback failed</string> <string name="error_generic">Playback failed</string>
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</string> <string name="error_drm_unsupported_before_api_18">DRM content not supported on API levels below 18</string>
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
<string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string> <string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
<string name="error_drm_unknown">An unknown DRM error occurred</string>
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string> <string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string> <string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
...@@ -51,15 +45,13 @@ ...@@ -51,15 +45,13 @@
<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> <string name="unsupported_ads_in_playlist">Playing without ads, as ads are not supported in playlists</string>
<string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string> <string name="download_start_error_offline_license">Failed to obtain offline license</string>
<string name="download_drm_unsupported">This demo app does not support downloading protected content</string> <string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string> <string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
...@@ -69,8 +61,4 @@ ...@@ -69,8 +61,4 @@
<string name="prefer_extension_decoders">Prefer extension decoders</string> <string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="random_abr">Enable random ABR</string>
<string name="tunneling">Request multimedia tunneling</string>
</resources> </resources>
...@@ -23,8 +23,4 @@ ...@@ -23,8 +23,4 @@
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
</style> </style>
<style name="PlayerTheme.Spherical">
<item name="surface_type">spherical_gl_surface_view</item>
</style>
</resources> </resources>
...@@ -18,4 +18,7 @@ called, and because you can move output off-screen easily (`setOutputSurface` ...@@ -18,4 +18,7 @@ called, and because you can move output off-screen easily (`setOutputSurface`
can't take a `null` surface, so the player has to use a `DummySurface`, which can't take a `null` surface, so the player has to use a `DummySurface`, which
doesn't handle protected output on all devices). doesn't handle protected output on all devices).
Please see the [demos README](../README.md) for instructions on how to build and
run this demo.
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl [SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl
...@@ -28,6 +28,7 @@ import android.widget.Button; ...@@ -28,6 +28,7 @@ import android.widget.Button;
import android.widget.GridLayout; import android.widget.GridLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
...@@ -183,13 +184,12 @@ public final class MainActivity extends Activity { ...@@ -183,13 +184,12 @@ public final class MainActivity extends Activity {
ACTION_VIEW.equals(action) ACTION_VIEW.equals(action)
? Assertions.checkNotNull(intent.getData()) ? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI); : Uri.parse(DEFAULT_MEDIA_URI);
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
DrmSessionManager drmSessionManager; DrmSessionManager drmSessionManager;
if (intent.hasExtra(DRM_SCHEME_EXTRA)) { if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
HttpMediaDrmCallback drmCallback = HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager = drmSessionManager =
...@@ -200,21 +200,19 @@ public final class MainActivity extends Activity { ...@@ -200,21 +200,19 @@ public final class MainActivity extends Activity {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
} }
DataSource.Factory dataSourceFactory = DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
new DefaultDataSourceFactory(
this, Util.getUserAgent(this, getString(R.string.application_name)));
MediaSource mediaSource; MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) { if (type == C.TYPE_DASH) {
mediaSource = mediaSource =
new DashMediaSource.Factory(dataSourceFactory) new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager) .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri); .createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) { } else if (type == C.TYPE_OTHER) {
mediaSource = mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory) new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager) .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri); .createMediaSource(MediaItem.fromUri(uri));
} else { } else {
throw new IllegalStateException(); throw new IllegalStateException();
} }
......
/* /*
* Copyright (C) 2018 The Android Open Source Project * Copyright (C) 2020 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -13,23 +13,7 @@ ...@@ -13,23 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.source.smoothstreaming.manifest; @NonNullApi
package com.google.android.exoplayer2.surfacedemo;
import android.net.Uri; import com.google.android.exoplayer2.util.NonNullApi;
import com.google.android.exoplayer2.util.Util;
/** SmoothStreaming related utility methods. */
public final class SsUtil {
/** Returns a fixed SmoothStreaming client manifest {@link Uri}. */
public static Uri fixManifestUri(Uri manifestUri) {
String lastPathSegment = manifestUri.getLastPathSegment();
if (lastPathSegment != null
&& Util.toLowerInvariant(lastPathSegment).matches("manifest(\\(.+\\))?")) {
return manifestUri;
}
return Uri.withAppendedPath(manifestUri, "Manifest");
}
private SsUtil() {}
}
...@@ -39,7 +39,7 @@ git clone https://github.com/google/cpu_features ...@@ -39,7 +39,7 @@ git clone https://github.com/google/cpu_features
``` ```
cd "${AV1_EXT_PATH}/jni" && \ cd "${AV1_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1 git clone https://chromium.googlesource.com/codecs/libgav1
``` ```
* Fetch Abseil: * Fetch Abseil:
...@@ -109,19 +109,22 @@ To try out playback using the extension in the [demo application][], see ...@@ -109,19 +109,22 @@ To try out playback using the extension in the [demo application][], see
There are two possibilities for rendering the output `Libgav1VideoRenderer` There are two possibilities for rendering the output `Libgav1VideoRenderer`
gets from the libgav1 decoder: gets from the libgav1 decoder:
* GL rendering using GL shader for color space conversion * GL rendering using GL shader for color space conversion
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
setting `surface_type` of `PlayerView` to be * If you are using `SimpleExoPlayer` with `PlayerView`, enable this option
`video_decoder_gl_surface_view`. by setting `surface_type` of `PlayerView` to be
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message `video_decoder_gl_surface_view`.
of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of * Otherwise, enable this option by sending `Libgav1VideoRenderer` a
`VideoDecoderOutputBufferRenderer` as its object. message of type `Renderer.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER`
with an instance of `VideoDecoderOutputBufferRenderer` as its object.
* Native rendering using `ANativeWindow`
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled * Native rendering using `ANativeWindow`
by default.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of * If you are using `SimpleExoPlayer` with `PlayerView`, this option is
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object. enabled by default.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
message of type `Renderer.MSG_SET_SURFACE` with an instance of
`SurfaceView` as its object.
Note: Although the default option uses `ANativeWindow`, based on our testing the Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred GL rendering mode has better performance, so should be preferred
......
...@@ -11,22 +11,10 @@ ...@@ -11,22 +11,10 @@
// 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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
externalNativeBuild { externalNativeBuild {
cmake { cmake {
// Debug CMake build type causes video frames to drop, // Debug CMake build type causes video frames to drop,
...@@ -36,30 +24,22 @@ android { ...@@ -36,30 +24,22 @@ android {
} }
} }
} }
// This option resolves the problem of finding libgav1JNI.so
// on multiple paths. The first one found is picked.
packagingOptions {
pickFirst 'lib/arm64-v8a/libgav1JNI.so'
pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
pickFirst 'lib/x86/libgav1JNI.so'
pickFirst 'lib/x86_64/libgav1JNI.so'
}
sourceSets.main {
// As native JNI library build is invoked from gradle, this is
// not needed. However, it exposes the built library and keeps
// consistency with the other extensions.
jniLibs.srcDir 'src/main/libs'
}
} }
// Configure the native build only if libgav1 is present, to avoid gradle sync // Configure the native build only if libgav1 is present to avoid gradle sync
// failures if libgav1 hasn't been checked out according to the README and CMake // failures if libgav1 hasn't been built according to the README instructions.
// isn't installed.
if (project.file('src/main/jni/libgav1').exists()) { if (project.file('src/main/jni/libgav1').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt' android.externalNativeBuild.cmake {
android.externalNativeBuild.cmake.version = '3.7.1+' path = 'src/main/jni/CMakeLists.txt'
version = '3.7.1+'
if (project.hasProperty('externalNativeBuildDir')) {
if (!new File(externalNativeBuildDir).isAbsolute()) {
ext.externalNativeBuildDir =
new File(rootDir, it.externalNativeBuildDir)
}
buildStagingDirectory = "${externalNativeBuildDir}/${project.name}"
}
}
} }
dependencies { dependencies {
......
...@@ -20,6 +20,7 @@ import static java.lang.Runtime.getRuntime; ...@@ -20,6 +20,7 @@ import static java.lang.Runtime.getRuntime;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
...@@ -83,18 +84,9 @@ import java.nio.ByteBuffer; ...@@ -83,18 +84,9 @@ import java.nio.ByteBuffer;
return "libgav1"; return "libgav1";
} }
/**
* Sets the output mode for frames rendered by the decoder.
*
* @param outputMode The output mode.
*/
public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override @Override
protected VideoDecoderInputBuffer createInputBuffer() { protected VideoDecoderInputBuffer createInputBuffer() {
return new VideoDecoderInputBuffer(); return new VideoDecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
} }
@Override @Override
...@@ -128,7 +120,7 @@ import java.nio.ByteBuffer; ...@@ -128,7 +120,7 @@ import java.nio.ByteBuffer;
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
} }
if (!decodeOnly) { if (!decodeOnly) {
outputBuffer.colorInfo = inputBuffer.colorInfo; outputBuffer.format = inputBuffer.format;
} }
return null; return null;
...@@ -156,6 +148,15 @@ import java.nio.ByteBuffer; ...@@ -156,6 +148,15 @@ import java.nio.ByteBuffer;
} }
/** /**
* Sets the output mode for frames rendered by the decoder.
*
* @param outputMode The output mode.
*/
public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
/**
* Renders output buffer to the given surface. Must only be called when in {@link * Renders output buffer to the given surface. Must only be called when in {@link
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode. * C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
* *
......
...@@ -126,7 +126,7 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer { ...@@ -126,7 +126,7 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer {
|| !Gav1Library.isAvailable()) { || !Gav1Library.isAvailable()) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
} }
if (format.drmInitData != null && format.exoMediaCryptoType == null) { if (format.exoMediaCryptoType != null) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
} }
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
......
# libgav1JNI requires modern CMake.
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR) cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
# libgav1JNI requires C++11.
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD 11)
project(libgav1JNI C CXX) project(libgav1JNI C CXX)
...@@ -21,24 +18,13 @@ if(build_type MATCHES "^rel") ...@@ -21,24 +18,13 @@ if(build_type MATCHES "^rel")
endif() endif()
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}") set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
set(libgav1_jni_output_directory
${libgav1_jni_root}/../libs/${ANDROID_ABI}/)
set(libgav1_root "${libgav1_jni_root}/libgav1")
set(libgav1_build "${libgav1_jni_build}/libgav1")
set(cpu_features_root "${libgav1_jni_root}/cpu_features")
set(cpu_features_build "${libgav1_jni_build}/cpu_features")
# Build cpu_features library. # Build cpu_features library.
add_subdirectory("${cpu_features_root}" add_subdirectory("${libgav1_jni_root}/cpu_features"
"${cpu_features_build}"
EXCLUDE_FROM_ALL) EXCLUDE_FROM_ALL)
# Build libgav1. # Build libgav1.
add_subdirectory("${libgav1_root}" add_subdirectory("${libgav1_jni_root}/libgav1"
"${libgav1_build}"
EXCLUDE_FROM_ALL) EXCLUDE_FROM_ALL)
# Build libgav1JNI. # Build libgav1JNI.
...@@ -58,7 +44,3 @@ target_link_libraries(gav1JNI ...@@ -58,7 +44,3 @@ target_link_libraries(gav1JNI
PRIVATE libgav1_static PRIVATE libgav1_static
PRIVATE ${android_log_lib}) PRIVATE ${android_log_lib})
# Specify output directory for libgav1JNI.
set_target_properties(gav1JNI PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
${libgav1_jni_output_directory})
...@@ -11,24 +11,7 @@ ...@@ -11,24 +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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:18.1.0' api 'com.google.android.gms:play-services-cast-framework:18.1.0'
...@@ -36,7 +19,7 @@ dependencies { ...@@ -36,7 +19,7 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import static java.lang.Math.min;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer; import com.google.android.exoplayer2.BasePlayer;
...@@ -30,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; ...@@ -30,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -290,6 +293,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -290,6 +293,7 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void addListener(EventListener listener) { public void addListener(EventListener listener) {
Assertions.checkNotNull(listener);
listeners.addIfAbsent(new ListenerHolder(listener)); listeners.addIfAbsent(new ListenerHolder(listener));
} }
...@@ -333,7 +337,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -333,7 +337,7 @@ public final class CastPlayer extends BasePlayer {
&& toIndex <= currentTimeline.getWindowCount() && toIndex <= currentTimeline.getWindowCount()
&& newIndex >= 0 && newIndex >= 0
&& newIndex < currentTimeline.getWindowCount()); && newIndex < currentTimeline.getWindowCount());
newIndex = Math.min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex)); newIndex = min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
if (fromIndex == toIndex || fromIndex == newIndex) { if (fromIndex == toIndex || fromIndex == newIndex) {
// Do nothing. // Do nothing.
return; return;
...@@ -426,6 +430,9 @@ public final class CastPlayer extends BasePlayer { ...@@ -426,6 +430,9 @@ public final class CastPlayer extends BasePlayer {
return playWhenReady.value; return playWhenReady.value;
} }
// We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override @Override
public void seekTo(int windowIndex, long positionMs) { public void seekTo(int windowIndex, long positionMs) {
MediaStatus mediaStatus = getMediaStatus(); MediaStatus mediaStatus = getMediaStatus();
...@@ -451,33 +458,17 @@ public final class CastPlayer extends BasePlayer { ...@@ -451,33 +458,17 @@ public final class CastPlayer extends BasePlayer {
flushNotifications(); flushNotifications();
} }
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override @Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
// Unsupported by the RemoteMediaClient API. Do nothing. // Unsupported by the RemoteMediaClient API. Do nothing.
} }
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override @Override
public PlaybackParameters getPlaybackParameters() { public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT; return PlaybackParameters.DEFAULT;
} }
@Override @Override
public void setPlaybackSpeed(float playbackSpeed) {
// Unsupported by the RemoteMediaClient API. Do nothing.
}
@Override
public float getPlaybackSpeed() {
return Player.DEFAULT_PLAYBACK_SPEED;
}
@Override
public void stop(boolean reset) { public void stop(boolean reset) {
playbackState = STATE_IDLE; playbackState = STATE_IDLE;
if (remoteMediaClient != null) { if (remoteMediaClient != null) {
...@@ -514,6 +505,12 @@ public final class CastPlayer extends BasePlayer { ...@@ -514,6 +505,12 @@ public final class CastPlayer extends BasePlayer {
} }
@Override @Override
@Nullable
public TrackSelector getTrackSelector() {
return null;
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) { public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {
return; return;
...@@ -800,7 +797,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -800,7 +797,7 @@ public final class CastPlayer extends BasePlayer {
} }
return remoteMediaClient.queueLoad( return remoteMediaClient.queueLoad(
mediaQueueItems, mediaQueueItems,
Math.min(startWindowIndex, mediaQueueItems.length - 1), min(startWindowIndex, mediaQueueItems.length - 1),
getCastRepeatMode(repeatMode), getCastRepeatMode(repeatMode),
startPositionMs, startPositionMs,
/* customData= */ null); /* customData= */ null);
...@@ -874,7 +871,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -874,7 +871,7 @@ public final class CastPlayer extends BasePlayer {
return; return;
} }
if (this.remoteMediaClient != null) { if (this.remoteMediaClient != null) {
this.remoteMediaClient.removeListener(statusListener); this.remoteMediaClient.unregisterCallback(statusListener);
this.remoteMediaClient.removeProgressListener(statusListener); this.remoteMediaClient.removeProgressListener(statusListener);
} }
this.remoteMediaClient = remoteMediaClient; this.remoteMediaClient = remoteMediaClient;
...@@ -882,7 +879,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -882,7 +879,7 @@ public final class CastPlayer extends BasePlayer {
if (sessionAvailabilityListener != null) { if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionAvailable(); sessionAvailabilityListener.onCastSessionAvailable();
} }
remoteMediaClient.addListener(statusListener); remoteMediaClient.registerCallback(statusListener);
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
updateInternalStateAndNotifyIfChanged(); updateInternalStateAndNotifyIfChanged();
} else { } else {
...@@ -996,10 +993,8 @@ public final class CastPlayer extends BasePlayer { ...@@ -996,10 +993,8 @@ public final class CastPlayer extends BasePlayer {
// Internal classes. // Internal classes.
private final class StatusListener private final class StatusListener extends RemoteMediaClient.Callback
implements RemoteMediaClient.Listener, implements SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
SessionManagerListener<CastSession>,
RemoteMediaClient.ProgressListener {
// RemoteMediaClient.ProgressListener implementation. // RemoteMediaClient.ProgressListener implementation.
...@@ -1008,7 +1003,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -1008,7 +1003,7 @@ public final class CastPlayer extends BasePlayer {
lastReportedPositionMs = progressMs; lastReportedPositionMs = progressMs;
} }
// RemoteMediaClient.Listener implementation. // RemoteMediaClient.Callback implementation.
@Override @Override
public void onStatusUpdated() { public void onStatusUpdated() {
...@@ -1085,6 +1080,9 @@ public final class CastPlayer extends BasePlayer { ...@@ -1085,6 +1080,9 @@ public final class CastPlayer extends BasePlayer {
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> { private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
// We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override @Override
public void onResult(MediaChannelResult result) { public void onResult(MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode(); int statusCode = result.getStatus().getStatusCode();
......
...@@ -15,10 +15,12 @@ ...@@ -15,10 +15,12 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.net.Uri;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import java.util.Arrays; import java.util.Arrays;
...@@ -126,7 +128,7 @@ import java.util.Arrays; ...@@ -126,7 +128,7 @@ import java.util.Arrays;
boolean isDynamic = durationUs == C.TIME_UNSET; boolean isDynamic = durationUs == C.TIME_UNSET;
return window.set( return window.set(
/* uid= */ ids[windowIndex], /* uid= */ ids[windowIndex],
/* tag= */ ids[windowIndex], /* mediaItem= */ new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(),
/* manifest= */ null, /* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
......
...@@ -59,8 +59,7 @@ public class CastPlayerTest { ...@@ -59,8 +59,7 @@ public class CastPlayerTest {
private CastPlayer castPlayer; private CastPlayer castPlayer;
@SuppressWarnings("deprecation") private RemoteMediaClient.Callback remoteMediaClientCallback;
private RemoteMediaClient.Listener remoteMediaClientListener;
@Mock private RemoteMediaClient mockRemoteMediaClient; @Mock private RemoteMediaClient mockRemoteMediaClient;
@Mock private MediaStatus mockMediaStatus; @Mock private MediaStatus mockMediaStatus;
...@@ -76,7 +75,7 @@ public class CastPlayerTest { ...@@ -76,7 +75,7 @@ public class CastPlayerTest {
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>> private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
setResultCallbackArgumentCaptor; setResultCallbackArgumentCaptor;
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor; @Captor private ArgumentCaptor<RemoteMediaClient.Callback> callbackArgumentCaptor;
@Captor private ArgumentCaptor<MediaQueueItem[]> queueItemsArgumentCaptor; @Captor private ArgumentCaptor<MediaQueueItem[]> queueItemsArgumentCaptor;
...@@ -95,8 +94,8 @@ public class CastPlayerTest { ...@@ -95,8 +94,8 @@ public class CastPlayerTest {
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
castPlayer = new CastPlayer(mockCastContext); castPlayer = new CastPlayer(mockCastContext);
castPlayer.addListener(mockListener); castPlayer.addListener(mockListener);
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture()); verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture());
remoteMediaClientListener = listenerArgumentCaptor.getValue(); remoteMediaClientCallback = callbackArgumentCaptor.getValue();
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
...@@ -113,7 +112,7 @@ public class CastPlayerTest { ...@@ -113,7 +112,7 @@ public class CastPlayerTest {
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
// There is a status update in the middle, which should be hidden by masking. // There is a status update in the middle, which should be hidden by masking.
remoteMediaClientListener.onStatusUpdated(); remoteMediaClientCallback.onStatusUpdated();
verifyNoMoreInteractions(mockListener); verifyNoMoreInteractions(mockListener);
// Upon result, the remoteMediaClient has updated its state according to the play() call. // Upon result, the remoteMediaClient has updated its state according to the play() call.
...@@ -169,7 +168,7 @@ public class CastPlayerTest { ...@@ -169,7 +168,7 @@ public class CastPlayerTest {
public void playWhenReady_changesOnStatusUpdates() { public void playWhenReady_changesOnStatusUpdates() {
assertThat(castPlayer.getPlayWhenReady()).isFalse(); assertThat(castPlayer.getPlayWhenReady()).isFalse();
when(mockRemoteMediaClient.isPaused()).thenReturn(false); when(mockRemoteMediaClient.isPaused()).thenReturn(false);
remoteMediaClientListener.onStatusUpdated(); remoteMediaClientCallback.onStatusUpdated();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
assertThat(castPlayer.getPlayWhenReady()).isTrue(); assertThat(castPlayer.getPlayWhenReady()).isTrue();
...@@ -187,7 +186,7 @@ public class CastPlayerTest { ...@@ -187,7 +186,7 @@ public class CastPlayerTest {
// There is a status update in the middle, which should be hidden by masking. // There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated(); remoteMediaClientCallback.onStatusUpdated();
verifyNoMoreInteractions(mockListener); verifyNoMoreInteractions(mockListener);
// Upon result, the mediaStatus now exposes the new repeat mode. // Upon result, the mediaStatus now exposes the new repeat mode.
...@@ -209,7 +208,7 @@ public class CastPlayerTest { ...@@ -209,7 +208,7 @@ public class CastPlayerTest {
// There is a status update in the middle, which should be hidden by masking. // There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated(); remoteMediaClientCallback.onStatusUpdated();
verifyNoMoreInteractions(mockListener); verifyNoMoreInteractions(mockListener);
// Upon result, the repeat mode is ALL. The state should reflect that. // Upon result, the repeat mode is ALL. The state should reflect that.
...@@ -224,7 +223,7 @@ public class CastPlayerTest { ...@@ -224,7 +223,7 @@ public class CastPlayerTest {
public void repeatMode_changesOnStatusUpdates() { public void repeatMode_changesOnStatusUpdates() {
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
remoteMediaClientListener.onStatusUpdated(); remoteMediaClientCallback.onStatusUpdated();
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
} }
...@@ -494,6 +493,6 @@ public class CastPlayerTest { ...@@ -494,6 +493,6 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems); castPlayer.addMediaItems(mediaItems);
// Call listener to update the timeline of the player. // Call listener to update the timeline of the player.
remoteMediaClientListener.onQueueStatusUpdated(); remoteMediaClientCallback.onQueueStatusUpdated();
} }
} }
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import static org.mockito.Mockito.when;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
...@@ -105,18 +107,18 @@ public class CastTimelineTrackerTest { ...@@ -105,18 +107,18 @@ public class CastTimelineTrackerTest {
int[] itemIds, int currentItemId, long currentDurationMs) { int[] itemIds, int currentItemId, long currentDurationMs) {
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class); RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
MediaStatus status = Mockito.mock(MediaStatus.class); MediaStatus status = Mockito.mock(MediaStatus.class);
Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList()); when(status.getQueueItems()).thenReturn(Collections.emptyList());
Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status); when(remoteMediaClient.getMediaStatus()).thenReturn(status);
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs)); when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId); when(status.getCurrentItemId()).thenReturn(currentItemId);
MediaQueue mediaQueue = mockMediaQueue(itemIds); MediaQueue mediaQueue = mockMediaQueue(itemIds);
Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue); when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
return remoteMediaClient; return remoteMediaClient;
} }
private static MediaQueue mockMediaQueue(int[] itemIds) { private static MediaQueue mockMediaQueue(int[] itemIds) {
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class); MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds); when(mediaQueue.getItemIds()).thenReturn(itemIds);
return mediaQueue; return mediaQueue;
} }
......
...@@ -11,29 +11,19 @@ ...@@ -11,29 +11,19 @@
// 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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies { dependencies {
api "com.google.android.gms:play-services-cronet:17.0.0" api "com.google.android.gms:play-services-cronet:17.0.0"
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import static java.lang.Math.min;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.chromium.net.UploadDataProvider; import org.chromium.net.UploadDataProvider;
...@@ -40,7 +42,7 @@ import org.chromium.net.UploadDataSink; ...@@ -40,7 +42,7 @@ import org.chromium.net.UploadDataSink;
@Override @Override
public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException { public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException {
int readLength = Math.min(byteBuffer.remaining(), data.length - position); int readLength = min(byteBuffer.remaining(), data.length - position);
byteBuffer.put(data, position, readLength); byteBuffer.put(data, position, readLength);
position += readLength; position += readLength;
uploadDataSink.onReadSucceeded(false); uploadDataSink.onReadSucceeded(false);
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
...@@ -50,14 +52,13 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -50,14 +52,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
private final HttpDataSource.Factory fallbackFactory; private final HttpDataSource.Factory fallbackFactory;
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
* fallback {@link HttpDataSource.Factory} will be used instead. * fallback {@link HttpDataSource.Factory} will be used instead.
* *
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
* cross-protocol redirects.
* *
* @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
...@@ -79,23 +80,36 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -79,23 +80,36 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
*
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
* DefaultHttpDataSourceFactory} will be used instead.
*
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
*/
public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor) {
this(cronetEngineWrapper, executor, DEFAULT_USER_AGENT);
}
/**
* Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
* DefaultHttpDataSourceFactory} will be used instead. * DefaultHttpDataSourceFactory} will be used instead.
* *
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
* cross-protocol redirects.
* *
* @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param userAgent A user agent used to create a fallback HttpDataSource if needed. * @param userAgent A user agent used to create a fallback HttpDataSource if needed.
*/ */
public CronetDataSourceFactory( public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper, CronetEngineWrapper cronetEngineWrapper, Executor executor, String userAgent) {
Executor executor,
String userAgent) {
this( this(
cronetEngineWrapper, cronetEngineWrapper,
executor, executor,
...@@ -112,7 +126,7 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -112,7 +126,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
* DefaultHttpDataSourceFactory} will be used instead. * DefaultHttpDataSourceFactory} will be used instead.
...@@ -147,7 +161,7 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -147,7 +161,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
* fallback {@link HttpDataSource.Factory} will be used instead. * fallback {@link HttpDataSource.Factory} will be used instead.
...@@ -178,14 +192,13 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -178,14 +192,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
* fallback {@link HttpDataSource.Factory} will be used instead. * fallback {@link HttpDataSource.Factory} will be used instead.
* *
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
* cross-protocol redirects.
* *
* @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
...@@ -209,14 +222,33 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -209,14 +222,33 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
*
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
* DefaultHttpDataSourceFactory} will be used instead.
*
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
*
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
* @param transferListener An optional listener.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
@Nullable TransferListener transferListener) {
this(cronetEngineWrapper, executor, transferListener, DEFAULT_USER_AGENT);
}
/**
* Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
* DefaultHttpDataSourceFactory} will be used instead. * DefaultHttpDataSourceFactory} will be used instead.
* *
* <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, * <p>Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout,
* {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout.
* cross-protocol redirects.
* *
* @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param cronetEngineWrapper A {@link CronetEngineWrapper}.
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
...@@ -244,7 +276,7 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -244,7 +276,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link
* DefaultHttpDataSourceFactory} will be used instead. * DefaultHttpDataSourceFactory} will be used instead.
...@@ -277,7 +309,7 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -277,7 +309,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
/** /**
* Constructs a CronetDataSourceFactory. * Creates an instance.
* *
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
* fallback {@link HttpDataSource.Factory} will be used instead. * fallback {@link HttpDataSource.Factory} will be used instead.
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import static java.lang.Math.min;
import android.content.Context; import android.content.Context;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -230,7 +232,7 @@ public final class CronetEngineWrapper { ...@@ -230,7 +232,7 @@ public final class CronetEngineWrapper {
} }
String[] versionStringsLeft = Util.split(versionLeft, "\\."); String[] versionStringsLeft = Util.split(versionLeft, "\\.");
String[] versionStringsRight = Util.split(versionRight, "\\."); String[] versionStringsRight = Util.split(versionRight, "\\.");
int minLength = Math.min(versionStringsLeft.length, versionStringsRight.length); int minLength = min(versionStringsLeft.length, versionStringsRight.length);
for (int i = 0; i < minLength; i++) { for (int i = 0; i < minLength; i++) {
if (!versionStringsLeft[i].equals(versionStringsRight[i])) { if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
try { try {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.min;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
...@@ -64,13 +65,10 @@ import org.junit.runner.RunWith; ...@@ -64,13 +65,10 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers; import org.mockito.ArgumentMatchers;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowLooper;
/** Tests for {@link CronetDataSource}. */ /** Tests for {@link CronetDataSource}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@LooperMode(Mode.PAUSED)
public final class CronetDataSourceTest { public final class CronetDataSourceTest {
private static final int TEST_CONNECT_TIMEOUT_MS = 100; private static final int TEST_CONNECT_TIMEOUT_MS = 100;
...@@ -378,15 +376,18 @@ public final class CronetDataSourceTest { ...@@ -378,15 +376,18 @@ public final class CronetDataSourceTest {
} }
@Test @Test
public void requestOpenValidatesStatusCode() { public void requestOpenPropagatesFailureResponseBody() throws Exception {
mockResponseStartSuccess(); mockResponseStartSuccess();
testUrlResponseInfo = createUrlResponseInfo(500); // statusCode // Use a size larger than CronetDataSource.READ_BUFFER_SIZE_BYTES
int responseLength = 40 * 1024;
mockReadSuccess(/* position= */ 0, /* length= */ responseLength);
testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 500);
try { try {
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected"); fail("HttpDataSource.InvalidResponseCodeException expected");
} catch (HttpDataSourceException e) { } catch (HttpDataSource.InvalidResponseCodeException e) {
assertThat(e).isInstanceOf(HttpDataSource.InvalidResponseCodeException.class); assertThat(e.responseBody).isEqualTo(buildTestDataArray(0, responseLength));
// Check for connection not automatically closed. // Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel(); verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never()) verify(mockTransferListener, never())
...@@ -1423,7 +1424,7 @@ public final class CronetDataSourceTest { ...@@ -1423,7 +1424,7 @@ public final class CronetDataSourceTest {
mockUrlRequest, testUrlResponseInfo); mockUrlRequest, testUrlResponseInfo);
} else { } else {
ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); int readLength = min(positionAndRemaining[1], inputBuffer.remaining());
inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
positionAndRemaining[0] += readLength; positionAndRemaining[0] += readLength;
positionAndRemaining[1] -= readLength; positionAndRemaining[1] -= readLength;
......
...@@ -18,14 +18,15 @@ its modules locally. Instructions for doing this can be found in ExoPlayer's ...@@ -18,14 +18,15 @@ its modules locally. Instructions for doing this can be found in ExoPlayer's
[top level README][]. The extension is not provided via JCenter (see [#2781][] [top level README][]. The extension is not provided via JCenter (see [#2781][]
for more information). for more information).
In addition, it's necessary to build the extension's native components as In addition, it's necessary to manually build the FFmpeg library, so that gradle
follows: can bundle the FFmpeg binaries in the APK:
* Set the following shell variable: * Set the following shell variable:
``` ```
cd "<path to exoplayer checkout>" cd "<path to exoplayer checkout>"
FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" EXOPLAYER_ROOT="$(pwd)"
FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
``` ```
* Download the [Android NDK][] and set its location in a shell variable. * Download the [Android NDK][] and set its location in a shell variable.
...@@ -41,6 +42,17 @@ NDK_PATH="<path to Android NDK>" ...@@ -41,6 +42,17 @@ NDK_PATH="<path to Android NDK>"
HOST_PLATFORM="linux-x86_64" HOST_PLATFORM="linux-x86_64"
``` ```
* Fetch FFmpeg and checkout an appropriate branch. We cannot guarantee
compatibility with all versions of FFmpeg. We currently recommend version 4.2:
```
cd "<preferred location for ffmpeg>" && \
git clone git://source.ffmpeg.org/ffmpeg && \
cd ffmpeg && \
git checkout release/4.2 && \
FFMPEG_PATH="$(pwd)"
```
* Configure the decoders to include. See the [Supported formats][] page for * Configure the decoders to include. See the [Supported formats][] page for
details of the available decoders, and which formats they support. details of the available decoders, and which formats they support.
...@@ -48,22 +60,21 @@ HOST_PLATFORM="linux-x86_64" ...@@ -48,22 +60,21 @@ HOST_PLATFORM="linux-x86_64"
ENABLED_DECODERS=(vorbis opus flac) ENABLED_DECODERS=(vorbis opus flac)
``` ```
* Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build * Add a link to the FFmpeg source code in the FFmpeg extension `jni` directory.
FFmpeg 4.2 for `armeabi-v7a`, `arm64-v8a`, `x86` and `x86_64`. The script can
be edited if you need to build for different architectures.
``` ```
cd "${FFMPEG_EXT_PATH}" && \ cd "${FFMPEG_EXT_PATH}/jni" && \
./build_ffmpeg.sh \ ln -s "$FFMPEG_PATH" ffmpeg
"${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
``` ```
* Build the JNI native libraries, setting `APP_ABI` to include the architectures * Execute `build_ffmpeg.sh` to build FFmpeg for `armeabi-v7a`, `arm64-v8a`,
built in the previous step. For example: `x86` and `x86_64`. The script can be edited if you need to build for
different architectures:
``` ```
cd "${FFMPEG_EXT_PATH}" && \ cd "${FFMPEG_EXT_PATH}/jni" && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" -j4 ./build_ffmpeg.sh \
"${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
``` ```
## Build instructions (Windows) ## ## Build instructions (Windows) ##
......
...@@ -11,29 +11,13 @@ ...@@ -11,29 +11,13 @@
// 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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android { // Configure the native build only if ffmpeg is present to avoid gradle sync
compileSdkVersion project.ext.compileSdkVersion // failures if ffmpeg hasn't been built according to the README instructions.
if (project.file('src/main/jni/ffmpeg').exists()) {
compileOptions { android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
sourceCompatibility JavaVersion.VERSION_1_8 android.externalNativeBuild.cmake.version = '3.7.1+'
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
sourceSets.main {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
......
...@@ -52,10 +52,10 @@ import java.util.List; ...@@ -52,10 +52,10 @@ import java.util.List;
private volatile int sampleRate; private volatile int sampleRate;
public FfmpegAudioDecoder( public FfmpegAudioDecoder(
Format format,
int numInputBuffers, int numInputBuffers,
int numOutputBuffers, int numOutputBuffers,
int initialInputBufferSize, int initialInputBufferSize,
Format format,
boolean outputFloat) boolean outputFloat)
throws FfmpegDecoderException { throws FfmpegDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
...@@ -82,7 +82,9 @@ import java.util.List; ...@@ -82,7 +82,9 @@ import java.util.List;
@Override @Override
protected DecoderInputBuffer createInputBuffer() { protected DecoderInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); return new DecoderInputBuffer(
DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT,
FfmpegLibrary.getInputBufferPaddingSize());
} }
@Override @Override
......
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
*/ */
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY;
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
import static com.google.android.exoplayer2.audio.AudioSink.SINK_FORMAT_UNSUPPORTED;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -22,16 +26,17 @@ import com.google.android.exoplayer2.Format; ...@@ -22,16 +26,17 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.AudioSink.SinkFormatSupport;
import com.google.android.exoplayer2.audio.DecoderAudioRenderer; import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import com.google.android.exoplayer2.util.Util;
/** Decodes and renders audio using FFmpeg. */ /** Decodes and renders audio using FFmpeg. */
public final class FfmpegAudioRenderer extends DecoderAudioRenderer { public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioDecoder> {
private static final String TAG = "FfmpegAudioRenderer"; private static final String TAG = "FfmpegAudioRenderer";
...@@ -40,10 +45,6 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer { ...@@ -40,10 +45,6 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
/** The default input buffer size. */ /** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6; private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
private final boolean enableFloatOutput;
private @MonotonicNonNull FfmpegAudioDecoder decoder;
public FfmpegAudioRenderer() { public FfmpegAudioRenderer() {
this(/* eventHandler= */ null, /* eventListener= */ null); this(/* eventHandler= */ null, /* eventListener= */ null);
} }
...@@ -63,8 +64,7 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer { ...@@ -63,8 +64,7 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
this( this(
eventHandler, eventHandler,
eventListener, eventListener,
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors), new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors));
/* enableFloatOutput= */ false);
} }
/** /**
...@@ -74,21 +74,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer { ...@@ -74,21 +74,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
* null if delivery of events is not required. * null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioSink The sink to which audio will be output. * @param audioSink The sink to which audio will be output.
* @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the
* device/build and if the input format may have bit depth higher than 16-bit. When using
* 32-bit float output, any audio processing will be disabled, including playback speed/pitch
* adjustment.
*/ */
public FfmpegAudioRenderer( public FfmpegAudioRenderer(
@Nullable Handler eventHandler, @Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener, @Nullable AudioRendererEventListener eventListener,
AudioSink audioSink, AudioSink audioSink) {
boolean enableFloatOutput) {
super( super(
eventHandler, eventHandler,
eventListener, eventListener,
audioSink); audioSink);
this.enableFloatOutput = enableFloatOutput;
} }
@Override @Override
...@@ -102,9 +96,11 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer { ...@@ -102,9 +96,11 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
String mimeType = Assertions.checkNotNull(format.sampleMimeType); String mimeType = Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) { if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(mimeType) || !isOutputSupported(format)) { } else if (!FfmpegLibrary.supportsFormat(mimeType)
|| (!sinkSupportsFormat(format, C.ENCODING_PCM_16BIT)
&& !sinkSupportsFormat(format, C.ENCODING_PCM_FLOAT))) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) { } else if (format.exoMediaCryptoType != null) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
} else { } else {
return FORMAT_HANDLED; return FORMAT_HANDLED;
...@@ -123,15 +119,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer { ...@@ -123,15 +119,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
TraceUtil.beginSection("createFfmpegAudioDecoder"); TraceUtil.beginSection("createFfmpegAudioDecoder");
int initialInputBufferSize = int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
decoder = FfmpegAudioDecoder decoder =
new FfmpegAudioDecoder( new FfmpegAudioDecoder(
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format)); format, NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, shouldOutputFloat(format));
TraceUtil.endSection(); TraceUtil.endSection();
return decoder; return decoder;
} }
@Override @Override
public Format getOutputFormat() { public Format getOutputFormat(FfmpegAudioDecoder decoder) {
Assertions.checkNotNull(decoder); Assertions.checkNotNull(decoder);
return new Format.Builder() return new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW) .setSampleMimeType(MimeTypes.AUDIO_RAW)
...@@ -141,31 +137,36 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer { ...@@ -141,31 +137,36 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
.build(); .build();
} }
private boolean isOutputSupported(Format inputFormat) { /**
return shouldUseFloatOutput(inputFormat) * Returns whether the renderer's {@link AudioSink} supports the PCM format that will be output
|| supportsOutput(inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT); * from the decoder for the given input format and requested output encoding.
*/
private boolean sinkSupportsFormat(Format inputFormat, @C.PcmEncoding int pcmEncoding) {
return sinkSupportsFormat(
Util.getPcmFormat(pcmEncoding, inputFormat.channelCount, inputFormat.sampleRate));
} }
private boolean shouldUseFloatOutput(Format inputFormat) { private boolean shouldOutputFloat(Format inputFormat) {
Assertions.checkNotNull(inputFormat.sampleMimeType); if (!sinkSupportsFormat(inputFormat, C.ENCODING_PCM_16BIT)) {
if (!enableFloatOutput // We have no choice because the sink doesn't support 16-bit integer PCM.
|| !supportsOutput( return true;
inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_FLOAT)) {
return false;
} }
switch (inputFormat.sampleMimeType) {
case MimeTypes.AUDIO_RAW: @SinkFormatSupport
// For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit. int formatSupport =
return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT getSinkFormatSupport(
|| inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT Util.getPcmFormat(
|| inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT; C.ENCODING_PCM_FLOAT, inputFormat.channelCount, inputFormat.sampleRate));
case MimeTypes.AUDIO_AC3: switch (formatSupport) {
// AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding. case SINK_FORMAT_SUPPORTED_DIRECTLY:
return false; // AC-3 is always 16-bit, so there's no point using floating point. Assume that it's worth
// using for all other formats.
return !MimeTypes.AUDIO_AC3.equals(inputFormat.sampleMimeType);
case SINK_FORMAT_UNSUPPORTED:
case SINK_FORMAT_SUPPORTED_WITH_TRANSCODING:
default: default:
// For all other formats, assume that it's worth using 32-bit float encoding. // Always prefer 16-bit PCM if the sink does not provide direct support for floating point.
return true; return false;
} }
} }
} }
...@@ -16,10 +16,12 @@ ...@@ -16,10 +16,12 @@
package com.google.android.exoplayer2.ext.ffmpeg; package com.google.android.exoplayer2.ext.ffmpeg;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; 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.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* Configures and queries the underlying native library. * Configures and queries the underlying native library.
...@@ -33,7 +35,10 @@ public final class FfmpegLibrary { ...@@ -33,7 +35,10 @@ public final class FfmpegLibrary {
private static final String TAG = "FfmpegLibrary"; private static final String TAG = "FfmpegLibrary";
private static final LibraryLoader LOADER = private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg"); new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg_jni");
private static @MonotonicNonNull String version;
private static int inputBufferPaddingSize = C.LENGTH_UNSET;
private FfmpegLibrary() {} private FfmpegLibrary() {}
...@@ -58,7 +63,27 @@ public final class FfmpegLibrary { ...@@ -58,7 +63,27 @@ public final class FfmpegLibrary {
/** Returns the version of the underlying library if available, or null otherwise. */ /** Returns the version of the underlying library if available, or null otherwise. */
@Nullable @Nullable
public static String getVersion() { public static String getVersion() {
return isAvailable() ? ffmpegGetVersion() : null; if (!isAvailable()) {
return null;
}
if (version == null) {
version = ffmpegGetVersion();
}
return version;
}
/**
* Returns the required amount of padding for input buffers in bytes, or {@link C#LENGTH_UNSET} if
* the underlying library is not available.
*/
public static int getInputBufferPaddingSize() {
if (!isAvailable()) {
return C.LENGTH_UNSET;
}
if (inputBufferPaddingSize == C.LENGTH_UNSET) {
inputBufferPaddingSize = ffmpegGetInputBufferPaddingSize();
}
return inputBufferPaddingSize;
} }
/** /**
...@@ -130,6 +155,8 @@ public final class FfmpegLibrary { ...@@ -130,6 +155,8 @@ public final class FfmpegLibrary {
} }
private static native String ffmpegGetVersion(); private static native String ffmpegGetVersion();
private static native boolean ffmpegHasDecoder(String codecName);
private static native int ffmpegGetInputBufferPaddingSize();
private static native boolean ffmpegHasDecoder(String codecName);
} }
...@@ -38,7 +38,7 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; ...@@ -38,7 +38,7 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
*/ */
public final class FfmpegVideoRenderer extends DecoderVideoRenderer { public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
private static final String TAG = "FfmpegAudioRenderer"; private static final String TAG = "FfmpegVideoRenderer";
/** /**
* Creates a new instance. * Creates a new instance.
...@@ -76,7 +76,7 @@ public final class FfmpegVideoRenderer extends DecoderVideoRenderer { ...@@ -76,7 +76,7 @@ public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) { } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) { } else if (format.exoMediaCryptoType != null) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
} else { } else {
return RendererCapabilities.create( return RendererCapabilities.create(
......
#
# 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.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libavcodec
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libswresample
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libavutil
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := ffmpeg_jni.cc
LOCAL_C_INCLUDES := ffmpeg
LOCAL_SHARED_LIBRARIES := libavcodec libswresample libavutil
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
include $(BUILD_SHARED_LIBRARY)
#
# 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.
#
APP_OPTIM := release
APP_STL := c++_static
APP_CPPFLAGS := -frtti
APP_PLATFORM := android-9
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
# Enable C++11 features.
set(CMAKE_CXX_STANDARD 11)
project(libffmpeg_jni C CXX)
set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg")
set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}")
foreach(ffmpeg_lib avutil swresample avcodec)
set(ffmpeg_lib_filename lib${ffmpeg_lib}.so)
set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename})
add_library(
${ffmpeg_lib}
SHARED
IMPORTED)
set_target_properties(
${ffmpeg_lib} PROPERTIES
IMPORTED_LOCATION
${ffmpeg_lib_file_path})
endforeach()
include_directories(${ffmpeg_location})
find_library(android_log_lib log)
add_library(ffmpeg_jni
SHARED
ffmpeg_jni.cc)
target_link_libraries(ffmpeg_jni
PRIVATE android
PRIVATE avutil
PRIVATE swresample
PRIVATE avcodec
PRIVATE ${android_log_lib})
...@@ -41,10 +41,7 @@ for decoder in "${ENABLED_DECODERS[@]}" ...@@ -41,10 +41,7 @@ for decoder in "${ENABLED_DECODERS[@]}"
do do
COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}" COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}"
done done
cd "${FFMPEG_EXT_PATH}" cd "${FFMPEG_EXT_PATH}/jni/ffmpeg"
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg)
cd ffmpeg
git checkout release/4.2
./configure \ ./configure \
--libdir=android-libs/armeabi-v7a \ --libdir=android-libs/armeabi-v7a \
--arch=arm \ --arch=arm \
......
...@@ -113,6 +113,10 @@ LIBRARY_FUNC(jstring, ffmpegGetVersion) { ...@@ -113,6 +113,10 @@ LIBRARY_FUNC(jstring, ffmpegGetVersion) {
return env->NewStringUTF(LIBAVCODEC_IDENT); return env->NewStringUTF(LIBAVCODEC_IDENT);
} }
LIBRARY_FUNC(jint, ffmpegGetInputBufferPaddingSize) {
return (jint)AV_INPUT_BUFFER_PADDING_SIZE;
}
LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) { LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
return getCodecByName(env, codecName) != NULL; return getCodecByName(env, codecName) != NULL;
} }
......
...@@ -21,13 +21,22 @@ import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts; ...@@ -21,13 +21,22 @@ import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */ /**
* Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer} and {@link
* FfmpegVideoRenderer}.
*/
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class DefaultRenderersFactoryTest { public final class DefaultRenderersFactoryTest {
@Test @Test
public void createRenderers_instantiatesVpxRenderer() { public void createRenderers_instantiatesFfmpegAudioRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO); FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
} }
@Test
public void createRenderers_instantiatesFfmpegVideoRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegVideoRenderer.class, C.TRACK_TYPE_VIDEO);
}
} }
...@@ -11,24 +11,9 @@ ...@@ -11,24 +11,9 @@
// 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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
sourceSets { sourceSets {
main { main {
jniLibs.srcDir 'src/main/libs' jniLibs.srcDir 'src/main/libs'
...@@ -36,8 +21,6 @@ android { ...@@ -36,8 +21,6 @@ android {
} }
androidTest.assets.srcDir '../../testdata/src/test/assets/' androidTest.assets.srcDir '../../testdata/src/test/assets/'
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
......
...@@ -37,16 +37,16 @@ import org.junit.runner.RunWith; ...@@ -37,16 +37,16 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class FlacExtractorSeekTest { public final class FlacExtractorSeekTest {
private static final String TEST_FILE_SEEK_TABLE = "flac/bear.flac"; private static final String TEST_FILE_SEEK_TABLE = "media/flac/bear.flac";
private static final String TEST_FILE_BINARY_SEARCH = "flac/bear_one_metadata_block.flac"; private static final String TEST_FILE_BINARY_SEARCH = "media/flac/bear_one_metadata_block.flac";
private static final String TEST_FILE_UNSEEKABLE = "flac/bear_no_seek_table_no_num_samples.flac"; private static final String TEST_FILE_UNSEEKABLE =
"media/flac/bear_no_seek_table_no_num_samples.flac";
private static final int DURATION_US = 2_741_000; private static final int DURATION_US = 2_741_000;
private FlacExtractor extractor = new FlacExtractor(); private FlacExtractor extractor = new FlacExtractor();
private FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); private FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
private DefaultDataSource dataSource = private DefaultDataSource dataSource =
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()).createDataSource();
.createDataSource();
@Test @Test
public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException { public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException {
......
...@@ -39,78 +39,80 @@ public class FlacExtractorTest { ...@@ -39,78 +39,80 @@ public class FlacExtractorTest {
@Test @Test
public void sample() throws Exception { public void sample() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, /* file= */ "flac/bear.flac", /* dumpFilesPrefix= */ "flac/bear_raw"); FlacExtractor::new,
/* file= */ "media/flac/bear.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_raw");
} }
@Test @Test
public void sampleWithId3HeaderAndId3Enabled() throws Exception { public void sampleWithId3HeaderAndId3Enabled() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_with_id3.flac", /* file= */ "media/flac/bear_with_id3.flac",
/* dumpFilesPrefix= */ "flac/bear_with_id3_enabled_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_enabled_raw");
} }
@Test @Test
public void sampleWithId3HeaderAndId3Disabled() throws Exception { public void sampleWithId3HeaderAndId3Disabled() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA), () -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
/* file= */ "flac/bear_with_id3.flac", /* file= */ "media/flac/bear_with_id3.flac",
/* dumpFilesPrefix= */ "flac/bear_with_id3_disabled_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_disabled_raw");
} }
@Test @Test
public void sampleUnseekable() throws Exception { public void sampleUnseekable() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_no_seek_table_no_num_samples.flac", /* file= */ "media/flac/bear_no_seek_table_no_num_samples.flac",
/* dumpFilesPrefix= */ "flac/bear_no_seek_table_no_num_samples_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_no_seek_table_no_num_samples_raw");
} }
@Test @Test
public void sampleWithVorbisComments() throws Exception { public void sampleWithVorbisComments() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_with_vorbis_comments.flac", /* file= */ "media/flac/bear_with_vorbis_comments.flac",
/* dumpFilesPrefix= */ "flac/bear_with_vorbis_comments_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_with_vorbis_comments_raw");
} }
@Test @Test
public void sampleWithPicture() throws Exception { public void sampleWithPicture() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_with_picture.flac", /* file= */ "media/flac/bear_with_picture.flac",
/* dumpFilesPrefix= */ "flac/bear_with_picture_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_with_picture_raw");
} }
@Test @Test
public void oneMetadataBlock() throws Exception { public void oneMetadataBlock() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_one_metadata_block.flac", /* file= */ "media/flac/bear_one_metadata_block.flac",
/* dumpFilesPrefix= */ "flac/bear_one_metadata_block_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_one_metadata_block_raw");
} }
@Test @Test
public void noMinMaxFrameSize() throws Exception { public void noMinMaxFrameSize() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_no_min_max_frame_size.flac", /* file= */ "media/flac/bear_no_min_max_frame_size.flac",
/* dumpFilesPrefix= */ "flac/bear_no_min_max_frame_size_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_no_min_max_frame_size_raw");
} }
@Test @Test
public void noNumSamples() throws Exception { public void noNumSamples() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_no_num_samples.flac", /* file= */ "media/flac/bear_no_num_samples.flac",
/* dumpFilesPrefix= */ "flac/bear_no_num_samples_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_no_num_samples_raw");
} }
@Test @Test
public void uncommonSampleRate() throws Exception { public void uncommonSampleRate() throws Exception {
ExtractorAsserts.assertAllBehaviors( ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new, FlacExtractor::new,
/* file= */ "flac/bear_uncommon_sample_rate.flac", /* file= */ "media/flac/bear_uncommon_sample_rate.flac",
/* dumpFilesPrefix= */ "flac/bear_uncommon_sample_rate_raw"); /* dumpFilesPrefix= */ "extractordumps/flac/bear_uncommon_sample_rate_raw");
} }
} }
...@@ -25,6 +25,7 @@ import androidx.test.core.app.ApplicationProvider; ...@@ -25,6 +25,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.AudioSink;
...@@ -33,6 +34,7 @@ import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; ...@@ -33,6 +34,7 @@ import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.testutil.CapturingAudioSink; import com.google.android.exoplayer2.testutil.CapturingAudioSink;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
...@@ -69,7 +71,7 @@ public class FlacPlaybackTest { ...@@ -69,7 +71,7 @@ public class FlacPlaybackTest {
TestPlaybackRunnable testPlaybackRunnable = TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable( new TestPlaybackRunnable(
Uri.parse("asset:///" + fileName), Uri.parse("asset:///media/" + fileName),
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),
audioSink); audioSink);
Thread thread = new Thread(testPlaybackRunnable); Thread thread = new Thread(testPlaybackRunnable);
...@@ -79,8 +81,10 @@ public class FlacPlaybackTest { ...@@ -79,8 +81,10 @@ public class FlacPlaybackTest {
throw testPlaybackRunnable.playbackException; throw testPlaybackRunnable.playbackException;
} }
audioSink.assertOutput( DumpFileAsserts.assertOutput(
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump"); ApplicationProvider.getApplicationContext(),
audioSink,
"audiosinkdumps/" + fileName + ".audiosink.dump");
} }
private static class TestPlaybackRunnable implements Player.EventListener, Runnable { private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
...@@ -107,9 +111,8 @@ public class FlacPlaybackTest { ...@@ -107,9 +111,8 @@ public class FlacPlaybackTest {
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
new ProgressiveMediaSource.Factory( new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"), new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY)
MatroskaExtractor.FACTORY) .createMediaSource(MediaItem.fromUri(uri));
.createMediaSource(uri);
player.setMediaSource(mediaSource); player.setMediaSource(mediaSource);
player.prepare(); player.prepare();
player.play(); player.play();
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import static java.lang.Math.max;
import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.FlacStreamMetadata; import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
...@@ -74,7 +76,7 @@ import java.nio.ByteBuffer; ...@@ -74,7 +76,7 @@ import java.nio.ByteBuffer;
/* floorBytePosition= */ firstFramePosition, /* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength, /* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(), /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max( /* minimumSearchRange= */ max(
FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize)); FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni); this.decoderJni = Assertions.checkNotNull(decoderJni);
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import static java.lang.Math.min;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
...@@ -118,7 +120,7 @@ import java.nio.ByteBuffer; ...@@ -118,7 +120,7 @@ import java.nio.ByteBuffer;
public int read(ByteBuffer target) throws IOException { public int read(ByteBuffer target) throws IOException {
int byteCount = target.remaining(); int byteCount = target.remaining();
if (byteBufferData != null) { if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining()); byteCount = min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit(); int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount); byteBufferData.limit(byteBufferData.position() + byteCount);
target.put(byteBufferData); target.put(byteBufferData);
...@@ -126,7 +128,7 @@ import java.nio.ByteBuffer; ...@@ -126,7 +128,7 @@ import java.nio.ByteBuffer;
} else if (extractorInput != null) { } else if (extractorInput != null) {
ExtractorInput extractorInput = this.extractorInput; ExtractorInput extractorInput = this.extractorInput;
byte[] tempBuffer = Util.castNonNull(this.tempBuffer); byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE); byteCount = min(byteCount, TEMP_BUFFER_SIZE);
int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount); int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
if (read < 4) { if (read < 4) {
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
......
...@@ -53,6 +53,11 @@ public final class FlacExtractor implements Extractor { ...@@ -53,6 +53,11 @@ public final class FlacExtractor implements Extractor {
/** Factory that returns one extractor which is a {@link FlacExtractor}. */ /** Factory that returns one extractor which is a {@link FlacExtractor}. */
public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
// LINT.IfChange
/*
* Flags in the two FLAC extractors should be kept in sync. If we ever change this then
* DefaultExtractorsFactory will need modifying, because it currently assumes this is the case.
*/
/** /**
* Flags controlling the behavior of the extractor. Possible flag value is {@link * Flags controlling the behavior of the extractor. Possible flag value is {@link
* #FLAG_DISABLE_ID3_METADATA}. * #FLAG_DISABLE_ID3_METADATA}.
...@@ -68,7 +73,9 @@ public final class FlacExtractor implements Extractor { ...@@ -68,7 +73,9 @@ public final class FlacExtractor implements Extractor {
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required. * required.
*/ */
public static final int FLAG_DISABLE_ID3_METADATA = 1; public static final int FLAG_DISABLE_ID3_METADATA =
com.google.android.exoplayer2.extractor.flac.FlacExtractor.FLAG_DISABLE_ID3_METADATA;
// LINT.ThenChange(../../../../../../../../../../../library/extractor/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java)
private final ParsableByteArray outputBuffer; private final ParsableByteArray outputBuffer;
private final boolean id3MetadataDisabled; private final boolean id3MetadataDisabled;
...@@ -203,7 +210,7 @@ public final class FlacExtractor implements Extractor { ...@@ -203,7 +210,7 @@ public final class FlacExtractor implements Extractor {
if (this.streamMetadata == null) { if (this.streamMetadata == null) {
this.streamMetadata = streamMetadata; this.streamMetadata = streamMetadata;
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize()); outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.getData()));
binarySearchSeeker = binarySearchSeeker =
outputSeekMap( outputSeekMap(
flacDecoderJni, flacDecoderJni,
......
...@@ -25,26 +25,24 @@ import com.google.android.exoplayer2.audio.AudioSink; ...@@ -25,26 +25,24 @@ import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DecoderAudioRenderer; import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.extractor.FlacStreamMetadata; import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacConstants; import com.google.android.exoplayer2.util.FlacConstants;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Decodes and renders audio using the native Flac decoder. */ /** Decodes and renders audio using the native Flac decoder. */
public final class LibflacAudioRenderer extends DecoderAudioRenderer { public final class LibflacAudioRenderer extends DecoderAudioRenderer<FlacDecoder> {
private static final String TAG = "LibflacAudioRenderer"; private static final String TAG = "LibflacAudioRenderer";
private static final int NUM_BUFFERS = 16; private static final int NUM_BUFFERS = 16;
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
public LibflacAudioRenderer() { public LibflacAudioRenderer() {
this(/* eventHandler= */ null, /* eventListener= */ null); this(/* eventHandler= */ null, /* eventListener= */ null);
} }
/** /**
* Creates an instance.
*
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
...@@ -58,6 +56,8 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer { ...@@ -58,6 +56,8 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer {
} }
/** /**
* Creates an instance.
*
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
...@@ -85,24 +85,25 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer { ...@@ -85,24 +85,25 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer {
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} }
// Compute the PCM encoding that the FLAC decoder will output. // Compute the format that the FLAC decoder will output.
@C.PcmEncoding int pcmEncoding; Format outputFormat;
if (format.initializationData.isEmpty()) { if (format.initializationData.isEmpty()) {
// The initialization data might not be set if the format was obtained from a manifest (e.g. // The initialization data might not be set if the format was obtained from a manifest (e.g.
// for DASH playbacks) rather than directly from the media. In this case we assume // for DASH playbacks) rather than directly from the media. In this case we assume
// ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as // ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as
// long as the AudioSink supports it, which will always be true when using DefaultAudioSink. // long as the AudioSink supports it, which will always be true when using DefaultAudioSink.
pcmEncoding = C.ENCODING_PCM_16BIT; outputFormat =
Util.getPcmFormat(C.ENCODING_PCM_16BIT, format.channelCount, format.sampleRate);
} else { } else {
int streamMetadataOffset = int streamMetadataOffset =
FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE; FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE;
FlacStreamMetadata streamMetadata = FlacStreamMetadata streamMetadata =
new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset); new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset);
pcmEncoding = Util.getPcmEncoding(streamMetadata.bitsPerSample); outputFormat = getOutputFormat(streamMetadata);
} }
if (!supportsOutput(format.channelCount, format.sampleRate, pcmEncoding)) { if (!sinkSupportsFormat(outputFormat)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) { } else if (format.exoMediaCryptoType != null) {
return FORMAT_UNSUPPORTED_DRM; return FORMAT_UNSUPPORTED_DRM;
} else { } else {
return FORMAT_HANDLED; return FORMAT_HANDLED;
...@@ -115,19 +116,19 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer { ...@@ -115,19 +116,19 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer {
TraceUtil.beginSection("createFlacDecoder"); TraceUtil.beginSection("createFlacDecoder");
FlacDecoder decoder = FlacDecoder decoder =
new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
streamMetadata = decoder.getStreamMetadata();
TraceUtil.endSection(); TraceUtil.endSection();
return decoder; return decoder;
} }
@Override @Override
protected Format getOutputFormat() { protected Format getOutputFormat(FlacDecoder decoder) {
Assertions.checkNotNull(streamMetadata); return getOutputFormat(decoder.getStreamMetadata());
return new Format.Builder() }
.setSampleMimeType(MimeTypes.AUDIO_RAW)
.setChannelCount(streamMetadata.channels) private static Format getOutputFormat(FlacStreamMetadata streamMetadata) {
.setSampleRate(streamMetadata.sampleRate) return Util.getPcmFormat(
.setPcmEncoding(Util.getPcmEncoding(streamMetadata.bitsPerSample)) Util.getPcmEncoding(streamMetadata.bitsPerSample),
.build(); streamMetadata.channels,
streamMetadata.sampleRate);
} }
} }
...@@ -26,7 +26,7 @@ import org.junit.runner.RunWith; ...@@ -26,7 +26,7 @@ import org.junit.runner.RunWith;
public final class DefaultRenderersFactoryTest { public final class DefaultRenderersFactoryTest {
@Test @Test
public void createRenderers_instantiatesVpxRenderer() { public void createRenderers_instantiatesFlacRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO); LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
} }
......
...@@ -11,24 +11,9 @@ ...@@ -11,24 +11,9 @@
// 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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android { android.defaultConfig.minSdkVersion 19
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
......
...@@ -26,35 +26,29 @@ locally. Instructions for doing this can be found in ExoPlayer's ...@@ -26,35 +26,29 @@ locally. Instructions for doing this can be found in ExoPlayer's
## Using the extension ## ## Using the extension ##
To play ads alongside a single-window content `MediaSource`, prepare the player To use the extension, follow the instructions on the
with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content [Ad insertion page](https://exoplayer.dev/ad-insertion.html#declarative-ad-support)
`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag of the developer guide. The `AdsLoaderProvider` passed to the player's
URI from your ad campaign when creating the `ImaAdsLoader`. The IMA `DefaultMediaSourceFactory` should return an `ImaAdsLoader`. Note that the IMA
documentation includes some [sample ad tags][] for testing. Note that the IMA
extension only supports players which are accessed on the application's main extension only supports players which are accessed on the application's main
thread. thread.
Resuming the player after entering the background requires some special handling 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 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 background, and are recreated when returning to the foreground. When playing ads
playing ads it is necessary to persist ad playback state while in the background it is necessary to persist ad playback state while in the background by keeping
by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of a reference to the `ImaAdsLoader`. When re-entering the foreground, pass the
the same content/ads by passing it in when constructing the new same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called
`AdsMediaSource`. It is also important to persist the player position when to restore the state. It is also important to persist the player position when
entering the background by storing the value of `player.getContentPosition()`. entering the background by storing the value of `player.getContentPosition()`.
On returning to the foreground, seek to that position before preparing the new On returning to the foreground, seek to that position before preparing the new
player instance. Finally, it is important to call `ImaAdsLoader.release()` when player instance. Finally, it is important to call `ImaAdsLoader.release()` when
playback of the content/ads has finished and will not be resumed. playback has finished and will not be resumed.
You can try the IMA extension in the ExoPlayer demo app. To do this you must You can try the IMA extension in the ExoPlayer demo app, which has test content
select and build one of the `withExtensions` build variants of the demo app in in the "IMA sample ad tags" section of the sample chooser. The demo app's
Android Studio. You can find IMA test content in the "IMA sample ad tags" `PlayerActivity` also shows how to persist the `ImaAdsLoader` instance and the
section of the app. The demo app's `PlayerActivity` also shows how to persist player position when backgrounded during ad playback.
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
## Links ## ## Links ##
......
...@@ -11,22 +11,10 @@ ...@@ -11,22 +11,10 @@
// 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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
// Enable multidex for androidTests. // Enable multidex for androidTests.
multiDexEnabled true multiDexEnabled true
} }
...@@ -34,22 +22,42 @@ android { ...@@ -34,22 +22,42 @@ android {
sourceSets { sourceSets {
androidTest.assets.srcDir '../../testdata/src/test/assets/' androidTest.assets.srcDir '../../testdata/src/test/assets/'
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.18.1' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.19.4'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'com.android.support:multidex:1.0.3' androidTestImplementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
......
...@@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.view.Surface; import android.view.Surface;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -39,6 +38,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; ...@@ -39,6 +38,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule;
...@@ -49,7 +49,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; ...@@ -49,7 +49,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
...@@ -234,29 +234,26 @@ public final class ImaPlaybackTest { ...@@ -234,29 +234,26 @@ public final class ImaPlaybackTest {
@Override @Override
protected MediaSource buildSource( protected MediaSource buildSource(
HostActivity host, HostActivity host,
String userAgent,
DrmSessionManager drmSessionManager, DrmSessionManager drmSessionManager,
FrameLayout overlayFrameLayout) { FrameLayout overlayFrameLayout) {
Context context = host.getApplicationContext(); Context context = host.getApplicationContext();
DataSource.Factory dataSourceFactory = DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context);
new DefaultDataSourceFactory(
context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName()));
MediaSource contentMediaSource = MediaSource contentMediaSource =
DefaultMediaSourceFactory.newInstance(context) new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
.createMediaSource(MediaItem.fromUri(contentUri));
return new AdsMediaSource( return new AdsMediaSource(
contentMediaSource, contentMediaSource,
dataSourceFactory, dataSourceFactory,
Assertions.checkNotNull(imaAdsLoader), Assertions.checkNotNull(imaAdsLoader),
new AdViewProvider() { new AdViewProvider() {
@Override @Override
public ViewGroup getAdViewGroup() { public ViewGroup getAdViewGroup() {
return overlayFrameLayout; return overlayFrameLayout;
} }
@Override @Override
public View[] getAdOverlayViews() { public ImmutableList<AdsLoader.OverlayInfo> getAdOverlayInfos() {
return new View[0]; return ImmutableList.of();
} }
}); });
} }
......
/*
* Copyright (C) 2020 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.source.ads.AdPlaybackState;
import java.util.Arrays;
import java.util.List;
/**
* Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data.
*/
/* package */ final class AdPlaybackStateFactory {
private AdPlaybackStateFactory() {}
/**
* Construct an {@link AdPlaybackState} from the provided {@code cuePoints}.
*
* @param cuePoints The cue points of the ads in seconds.
* @return The {@link AdPlaybackState}.
*/
public static AdPlaybackState fromCuePoints(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
return new AdPlaybackState(/* adGroupTimesUs...= */ 0);
}
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
if (cuePoint == -1.0) {
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
} else {
adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint);
}
}
// Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return new AdPlaybackState(adGroupTimesUs);
}
}
# ExoPlayer Firebase JobDispatcher extension # # ExoPlayer Firebase JobDispatcher extension #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] **This extension is deprecated. Use the [WorkManager extension][] instead.**
instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md [WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ## ## Getting the extension ##
......
...@@ -13,24 +13,7 @@ ...@@ -13,24 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
apply from: '../../constants.gradle' apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
......
...@@ -60,11 +60,15 @@ import com.google.android.exoplayer2.util.Util; ...@@ -60,11 +60,15 @@ import com.google.android.exoplayer2.util.Util;
@Deprecated @Deprecated
public final class JobDispatcherScheduler implements Scheduler { public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false;
private static final String TAG = "JobDispatcherScheduler"; private static final String TAG = "JobDispatcherScheduler";
private static final String KEY_SERVICE_ACTION = "service_action"; private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package"; private static final String KEY_SERVICE_PACKAGE = "service_package";
private static final String KEY_REQUIREMENTS = "requirements"; private static final String KEY_REQUIREMENTS = "requirements";
private static final int SUPPORTED_REQUIREMENTS =
Requirements.NETWORK
| Requirements.NETWORK_UNMETERED
| Requirements.DEVICE_IDLE
| Requirements.DEVICE_CHARGING;
private final String jobTag; private final String jobTag;
private final FirebaseJobDispatcher jobDispatcher; private final FirebaseJobDispatcher jobDispatcher;
...@@ -85,35 +89,44 @@ public final class JobDispatcherScheduler implements Scheduler { ...@@ -85,35 +89,44 @@ public final class JobDispatcherScheduler implements Scheduler {
public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) { public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction); Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
int result = jobDispatcher.schedule(job); int result = jobDispatcher.schedule(job);
logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
} }
@Override @Override
public boolean cancel() { public boolean cancel() {
int result = jobDispatcher.cancel(jobTag); int result = jobDispatcher.cancel(jobTag);
logd("Canceling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
} }
@Override
public Requirements getSupportedRequirements(Requirements requirements) {
return requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
}
private static Job buildJob( private static Job buildJob(
FirebaseJobDispatcher dispatcher, FirebaseJobDispatcher dispatcher,
Requirements requirements, Requirements requirements,
String tag, String tag,
String servicePackage, String servicePackage,
String serviceAction) { String serviceAction) {
Requirements filteredRequirements = requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
if (!filteredRequirements.equals(requirements)) {
Log.w(
TAG,
"Ignoring unsupported requirements: "
+ (filteredRequirements.getRequirements() ^ requirements.getRequirements()));
}
Job.Builder builder = Job.Builder builder =
dispatcher dispatcher
.newJobBuilder() .newJobBuilder()
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called .setService(JobDispatcherSchedulerService.class) // the JobService that will be called
.setTag(tag); .setTag(tag);
if (requirements.isUnmeteredNetworkRequired()) { if (requirements.isUnmeteredNetworkRequired()) {
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK); builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
} else if (requirements.isNetworkRequired()) { } else if (requirements.isNetworkRequired()) {
builder.addConstraint(Constraint.ON_ANY_NETWORK); builder.addConstraint(Constraint.ON_ANY_NETWORK);
} }
if (requirements.isIdleRequired()) { if (requirements.isIdleRequired()) {
builder.addConstraint(Constraint.DEVICE_IDLE); builder.addConstraint(Constraint.DEVICE_IDLE);
} }
...@@ -131,31 +144,20 @@ public final class JobDispatcherScheduler implements Scheduler { ...@@ -131,31 +144,20 @@ public final class JobDispatcherScheduler implements Scheduler {
return builder.build(); return builder.build();
} }
private static void logd(String message) {
if (DEBUG) {
Log.d(TAG, message);
}
}
/** A {@link JobService} that starts the target service if the requirements are met. */ /** A {@link JobService} that starts the target service if the requirements are met. */
public static final class JobDispatcherSchedulerService extends JobService { public static final class JobDispatcherSchedulerService extends JobService {
@Override @Override
public boolean onStartJob(JobParameters params) { public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started"); Bundle extras = Assertions.checkNotNull(params.getExtras());
Bundle extras = params.getExtras();
Assertions.checkNotNull(extras, "Service started without extras.");
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS)); Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) { int notMetRequirements = requirements.getNotMetRequirements(this);
logd("Requirements are met"); if (notMetRequirements == 0) {
String serviceAction = extras.getString(KEY_SERVICE_ACTION); String serviceAction = Assertions.checkNotNull(extras.getString(KEY_SERVICE_ACTION));
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE); String servicePackage = Assertions.checkNotNull(extras.getString(KEY_SERVICE_PACKAGE));
Assertions.checkNotNull(serviceAction, "Service action missing.");
Assertions.checkNotNull(servicePackage, "Service package missing.");
Intent intent = new Intent(serviceAction).setPackage(servicePackage); Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(this, intent); Util.startForegroundService(this, intent);
} else { } else {
logd("Requirements are not met"); Log.w(TAG, "Requirements not met: " + notMetRequirements);
jobFinished(params, /* needsReschedule */ true); jobFinished(params, /* needsReschedule */ true);
} }
return false; return false;
......
...@@ -11,24 +11,9 @@ ...@@ -11,24 +11,9 @@
// 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 from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
apply plugin: 'com.android.library'
android { android.defaultConfig.minSdkVersion 17
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 17
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
......
...@@ -72,7 +72,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab ...@@ -72,7 +72,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
this.context = context; this.context = context;
this.player = player; this.player = player;
this.updatePeriodMs = updatePeriodMs; this.updatePeriodMs = updatePeriodMs;
handler = Util.createHandler(); handler = Util.createHandlerForCurrentOrMainLooper();
componentListener = new ComponentListener(); componentListener = new ComponentListener();
controlDispatcher = new DefaultControlDispatcher(); controlDispatcher = new DefaultControlDispatcher();
} }
......
# ExoPlayer Media2 extension #
The Media2 extension provides builders for [SessionPlayer][] and [MediaSession.SessionCallback][] in
the [Media2 library][].
Compared to [MediaSessionConnector][] that uses [MediaSessionCompat][], this provides finer grained
control for incoming calls, so you can selectively allow/reject commands per controller.
## Getting the extension ##
The easiest way to use the extension is to add it as a gradle dependency:
```gradle
implementation 'com.google.android.exoplayer:extension-media2:2.X.X'
```
where `2.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 ##
### Using `SessionPlayerConnector` ###
`SessionPlayerConnector` is a [SessionPlayer][] implementation wrapping a given `Player`.
You can use a [SessionPlayer][] instance to build a [MediaSession][], or to set the player
associated with a [VideoView][] or [MediaControlView][]
### Using `SessionCallbackBuilder` ###
`SessionCallbackBuilder` lets you build a [MediaSession.SessionCallback][] instance given its
collaborators. You can use a [MediaSession.SessionCallback][] to build a [MediaSession][].
## Links ##
* [Javadoc][]: Classes matching
`com.google.android.exoplayer2.ext.media2.*` belong to this module.
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
[SessionPlayer]: https://developer.android.com/reference/androidx/media2/common/SessionPlayer
[MediaSession]: https://developer.android.com/reference/androidx/media2/session/MediaSession
[MediaSession.SessionCallback]: https://developer.android.com/reference/androidx/media2/session/MediaSession.SessionCallback
[Media2 library]: https://developer.android.com/jetpack/androidx/releases/media2
[MediaSessionCompat]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat
[MediaSessionConnector]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.html
[VideoView]: https://developer.android.com/reference/androidx/media2/widget/VideoView
[MediaControlView]: https://developer.android.com/reference/androidx/media2/widget/MediaControlView
// Copyright 2019 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: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android.defaultConfig.minSdkVersion 19
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.collection:collection:' + androidxCollectionVersion
implementation 'androidx.concurrent:concurrent-futures:1.1.0'
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
api 'androidx.media2:media2-session:1.0.3'
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
androidTestImplementation 'androidx.test:core:' + androidxTestCoreVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'com.google.truth:truth:' + truthVersion
}
ext {
javadocTitle = 'Media2 extension'
}
apply from: '../../javadoc_library.gradle'
ext {
releaseArtifact = 'extension-media2'
releaseDescription = 'Media2 extension for ExoPlayer.'
}
apply from: '../../publish.gradle'
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.media2.test">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-sdk/>
<application
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<activity android:name="com.google.android.exoplayer2.ext.media2.MediaStubActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false"
android:label="MediaStubActivity"/>
</application>
<instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.media2.test"
android:name="androidx.test.runner.AndroidJUnitRunner"/>
</manifest>
/*
* Copyright 2020 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.media2;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.SessionPlayer.PlayerResult;
import androidx.media2.session.MediaSession;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.exoplayer2.ext.media2.test.R;
import com.google.android.exoplayer2.util.Assertions;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.CountDownLatch;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link MediaSessionUtil} */
@RunWith(AndroidJUnit4.class)
public class MediaSessionUtilTest {
private static final int PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
@Rule public final PlayerTestRule playerTestRule = new PlayerTestRule();
@Test
public void getSessionCompatToken_withMediaControllerCompat_returnsValidToken() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
SessionPlayerConnector sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();
MediaSession.SessionCallback sessionCallback =
new SessionCallbackBuilder(context, sessionPlayerConnector).build();
TestUtils.loadResource(R.raw.audio, sessionPlayerConnector);
ListenableFuture<PlayerResult> prepareResult = sessionPlayerConnector.prepare();
CountDownLatch latch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
playerTestRule.getExecutor(),
new SessionPlayer.PlayerCallback() {
@Override
public void onPlayerStateChanged(@NonNull SessionPlayer player, int playerState) {
if (playerState == SessionPlayer.PLAYER_STATE_PLAYING) {
latch.countDown();
}
}
});
MediaSession session2 =
new MediaSession.Builder(context, sessionPlayerConnector)
.setSessionCallback(playerTestRule.getExecutor(), sessionCallback)
.build();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
try {
MediaSessionCompat.Token token =
Assertions.checkNotNull(MediaSessionUtil.getSessionCompatToken(session2));
MediaControllerCompat controllerCompat = new MediaControllerCompat(context, token);
controllerCompat.getTransportControls().play();
} catch (Exception e) {
throw new IllegalStateException(e);
}
});
assertThat(prepareResult.get(PLAYER_STATE_CHANGE_WAIT_TIME_MS, MILLISECONDS).getResultCode())
.isEqualTo(PlayerResult.RESULT_SUCCESS);
assertThat(latch.await(PLAYER_STATE_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
}
}
/*
* Copyright 2019 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.media2;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import com.google.android.exoplayer2.ext.media2.test.R;
import com.google.android.exoplayer2.util.Util;
/** Stub activity to play media contents on. */
public final class MediaStubActivity extends Activity {
private static final String TAG = "MediaStubActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mediaplayer);
// disable enter animation.
overridePendingTransition(0, 0);
if (Util.SDK_INT >= 27) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setTurnScreenOn(true);
setShowWhenLocked(true);
KeyguardManager keyguardManager =
(KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
keyguardManager.requestDismissKeyguard(this, null);
} else {
getWindow()
.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
}
}
@Override
public void finish() {
super.finish();
// disable exit animation.
overridePendingTransition(0, 0);
}
@Override
protected void onResume() {
Log.i(TAG, "onResume");
super.onResume();
}
@Override
protected void onPause() {
Log.i(TAG, "onPause");
super.onPause();
}
public SurfaceHolder getSurfaceHolder() {
SurfaceView surface = findViewById(R.id.surface);
return surface.getHolder();
}
}
/*
* Copyright 2019 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.media2;
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.rules.ExternalResource;
/** Rule for tests that use {@link SessionPlayerConnector}. */
/* package */ final class PlayerTestRule extends ExternalResource {
/** Instrumentation to attach to {@link DataSource} instances used by the player. */
public interface DataSourceInstrumentation {
/** Called at the start of {@link DataSource#open}. */
void onPreOpen(DataSpec dataSpec);
}
private Context context;
private ExecutorService executor;
private SessionPlayerConnector sessionPlayerConnector;
private SimpleExoPlayer exoPlayer;
@Nullable private DataSourceInstrumentation dataSourceInstrumentation;
@Override
protected void before() {
// Workaround limitation in androidx.media2.session:1.0.3 which session can only be instantiated
// on thread with prepared Looper.
// TODO: Remove when androidx.media2.session:1.1.0 is released without the limitation
// [Internal: b/146536708]
if (Looper.myLooper() == null) {
Looper.prepare();
}
context = ApplicationProvider.getApplicationContext();
executor = Executors.newFixedThreadPool(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
// Initialize AudioManager on the main thread to workaround that
// audio focus listener is called on the thread where the AudioManager was
// originally initialized. [Internal: b/78617702]
// Without posting this, audio focus listeners wouldn't be called because the
// listeners would be posted to the test thread (here) where it waits until the
// tests are finished.
context.getSystemService(Context.AUDIO_SERVICE);
DataSource.Factory dataSourceFactory = new InstrumentingDataSourceFactory(context);
exoPlayer =
new SimpleExoPlayer.Builder(context)
.setLooper(Looper.myLooper())
.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory))
.build();
sessionPlayerConnector = new SessionPlayerConnector(exoPlayer);
});
}
@Override
protected void after() {
if (sessionPlayerConnector != null) {
sessionPlayerConnector.close();
sessionPlayerConnector = null;
}
if (exoPlayer != null) {
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
exoPlayer.release();
exoPlayer = null;
});
}
if (executor != null) {
executor.shutdown();
executor = null;
}
}
public void setDataSourceInstrumentation(
@Nullable DataSourceInstrumentation dataSourceInstrumentation) {
this.dataSourceInstrumentation = dataSourceInstrumentation;
}
public ExecutorService getExecutor() {
return executor;
}
public SessionPlayerConnector getSessionPlayerConnector() {
return sessionPlayerConnector;
}
public SimpleExoPlayer getSimpleExoPlayer() {
return exoPlayer;
}
private final class InstrumentingDataSourceFactory implements DataSource.Factory {
private final DefaultDataSourceFactory defaultDataSourceFactory;
public InstrumentingDataSourceFactory(Context context) {
defaultDataSourceFactory = new DefaultDataSourceFactory(context);
}
@Override
public DataSource createDataSource() {
DataSource dataSource = defaultDataSourceFactory.createDataSource();
return dataSourceInstrumentation == null
? dataSource
: new InstrumentedDataSource(dataSource, dataSourceInstrumentation);
}
}
private static final class InstrumentedDataSource implements DataSource {
private final DataSource wrappedDataSource;
private final DataSourceInstrumentation instrumentation;
public InstrumentedDataSource(
DataSource wrappedDataSource, DataSourceInstrumentation instrumentation) {
this.wrappedDataSource = wrappedDataSource;
this.instrumentation = instrumentation;
}
@Override
public void addTransferListener(TransferListener transferListener) {
wrappedDataSource.addTransferListener(transferListener);
}
@Override
public long open(DataSpec dataSpec) throws IOException {
instrumentation.onPreOpen(dataSpec);
return wrappedDataSource.open(dataSpec);
}
@Nullable
@Override
public Uri getUri() {
return wrappedDataSource.getUri();
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return wrappedDataSource.getResponseHeaders();
}
@Override
public int read(byte[] target, int offset, int length) throws IOException {
return wrappedDataSource.read(target, offset, length);
}
@Override
public void close() throws IOException {
wrappedDataSource.close();
}
}
}
/*
* Copyright 2019 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.media2;
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_SUCCESS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.SessionPlayer.PlayerResult;
import androidx.media2.common.UriMediaItem;
import com.google.android.exoplayer2.ext.media2.test.R;
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
/** Utilities for tests. */
/* package */ final class TestUtils {
private static final long PLAYER_STATE_CHANGE_WAIT_TIME_MS = 5_000;
public static UriMediaItem createMediaItem() {
return createMediaItem(R.raw.video_desks);
}
public static UriMediaItem createMediaItem(int resId) {
MediaMetadata metadata =
new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Integer.toString(resId))
.build();
return new UriMediaItem.Builder(RawResourceDataSource.buildRawResourceUri(resId))
.setMetadata(metadata)
.build();
}
public static List<MediaItem> createPlaylist(int size) {
List<MediaItem> items = new ArrayList<>();
for (int i = 0; i < size; ++i) {
items.add(createMediaItem());
}
return items;
}
public static void loadResource(int resId, SessionPlayer sessionPlayer) throws Exception {
MediaItem mediaItem = createMediaItem(resId);
assertPlayerResultSuccess(sessionPlayer.setMediaItem(mediaItem));
}
public static void assertPlayerResultSuccess(Future<PlayerResult> future) throws Exception {
assertPlayerResult(future, RESULT_SUCCESS);
}
public static void assertPlayerResult(
Future<PlayerResult> future, /* @PlayerResult.ResultCode */ int playerResult)
throws Exception {
assertThat(future).isNotNull();
PlayerResult result = future.get(PLAYER_STATE_CHANGE_WAIT_TIME_MS, MILLISECONDS);
assertThat(result).isNotNull();
assertThat(result.getResultCode()).isEqualTo(playerResult);
}
private TestUtils() {
// Prevent from instantiation.
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2019 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:keepScreenOn="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView android:id="@+id/surface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1">
</SurfaceView>
<SurfaceView android:id="@+id/surface2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1">
</SurfaceView>
<SurfaceView android:id="@+id/surface3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1">
</SurfaceView>
</LinearLayout>
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