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
# AV1 extension
extensions/av1/src/main/jni/libgav1
extensions/av1/src/main/jni/cpu_features
# Opus extension
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
alternative to Android’s MediaPlayer API for playing audio and video both
......
......@@ -17,7 +17,7 @@ buildscript {
jcenter()
}
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.google.android.gms:strict-version-matcher-plugin:1.2.1'
}
......@@ -26,6 +26,7 @@ allprojects {
repositories {
google()
jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
project.ext {
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 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.11.4'
releaseVersionCode = 2011004
releaseVersion = '2.12.0'
releaseVersionCode = 2012000
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
compileSdkVersion = 29
dexmakerVersion = '2.21.0'
junitVersion = '4.13-rc-2'
guavaVersion = '28.2-android'
mockitoVersion = '2.25.0'
robolectricVersion = '4.3.1'
checkerframeworkVersion = '2.5.0'
guavaVersion = '27.1-android'
mockitoVersion = '2.28.2'
mockWebServerVersion = '3.12.0'
robolectricVersion = '4.4'
checkerframeworkVersion = '3.3.0'
checkerframeworkCompatVersion = '2.5.0'
jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.3.70'
androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1'
androidxMultidexVersion = '2.0.0'
androidxRecyclerViewVersion = '1.1.0'
androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0'
......
......@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
def rootDir = gradle.ext.exoplayerRoot
if (!gradle.ext.has('exoplayerSettingsDir')) {
gradle.ext.exoplayerSettingsDir =
new File(rootDir.toString()).getCanonicalPath()
}
def modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix
......@@ -35,6 +39,7 @@ include modulePrefix + 'extension-ima'
include modulePrefix + 'extension-cast'
include modulePrefix + 'extension-cronet'
include modulePrefix + 'extension-mediasession'
include modulePrefix + 'extension-media2'
include modulePrefix + 'extension-okhttp'
include modulePrefix + 'extension-opus'
include modulePrefix + 'extension-vp9'
......@@ -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-cronet').projectDir = new File(rootDir, 'extensions/cronet')
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-opus').projectDir = new File(rootDir, 'extensions/opus')
project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9')
......
......@@ -2,3 +2,24 @@
This directory contains applications that demonstrate how to use ExoPlayer.
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 @@
This folder contains a demo application that showcases ExoPlayer integration
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 {
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
......@@ -57,8 +58,9 @@ dependencies {
implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast')
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
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'
/*
* 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;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
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.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
......@@ -61,7 +60,6 @@ import java.util.ArrayList;
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT);
private final DefaultMediaSourceFactory defaultMediaSourceFactory;
private final PlayerView localPlayerView;
private final PlayerControlView castControlView;
private final DefaultTrackSelector trackSelector;
......@@ -97,7 +95,6 @@ import java.util.ArrayList;
trackSelector = new DefaultTrackSelector(context);
exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
defaultMediaSourceFactory = DefaultMediaSourceFactory.newInstance(context, DATA_SOURCE_FACTORY);
exoPlayer.addListener(this);
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
timestamp, to show how to get the timestamp of the frame currently in the
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
......@@ -49,5 +49,5 @@ dependencies {
implementation project(modulePrefix + 'library-dash')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
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() {
gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a;
}
......@@ -11,11 +11,10 @@
// 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.
attribute vec4 a_position;
attribute vec3 a_texcoord;
attribute vec2 a_position;
attribute vec2 a_texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = a_position;
v_texcoord = a_texcoord.xy;
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
v_texcoord = a_texcoord;
}
......@@ -88,18 +88,9 @@ import javax.microedition.khronos.opengles.GL10;
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
for (GlUtil.Attribute attribute : attributes) {
if (attribute.name.equals("a_position")) {
attribute.setBuffer(
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);
attribute.setBuffer(new float[] {-1, -1, 1, -1, -1, 1, 1, 1}, 2);
} else if (attribute.name.equals("a_texcoord")) {
attribute.setBuffer(
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);
attribute.setBuffer(new float[] {0, 1, 1, 1, 0, 0, 1, 0}, 2);
}
}
this.attributes = attributes;
......
......@@ -24,6 +24,7 @@ import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
......@@ -138,13 +139,12 @@ public final class MainActivity extends Activity {
ACTION_VIEW.equals(action)
? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI);
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
DrmSessionManager drmSessionManager;
if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager =
......@@ -155,21 +155,19 @@ public final class MainActivity extends Activity {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
}
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(
this, Util.getUserAgent(this, getString(R.string.application_name)));
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
.createMediaSource(MediaItem.fromUri(uri));
} else {
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 @@
app:surface_type="none"/>
</FrameLayout>
......@@ -3,3 +3,6 @@
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
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 {
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
......@@ -49,34 +50,46 @@ android {
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
}
flavorDimensions "extensions"
flavorDimensions "decoderExtensions"
productFlavors {
noExtensions {
dimension "extensions"
noDecoderExtensions {
dimension "decoderExtensions"
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "false"
}
withExtensions {
dimension "extensions"
withDecoderExtensions {
dimension "decoderExtensions"
buildConfigField "boolean", "USE_DECODER_EXTENSIONS", "true"
}
}
}
dependencies {
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
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-dash')
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui')
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
withExtensionsImplementation project(path: modulePrefix + 'extension-opus')
withExtensionsImplementation project(path: modulePrefix + 'extension-vp9')
withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp')
implementation project(modulePrefix + 'extension-cronet')
implementation project(modulePrefix + 'extension-ima')
withDecoderExtensionsImplementation project(modulePrefix + 'extension-av1')
withDecoderExtensionsImplementation project(modulePrefix + 'extension-ffmpeg')
withDecoderExtensionsImplementation project(modulePrefix + 'extension-flac')
withDecoderExtensionsImplementation project(modulePrefix + 'extension-opus')
withDecoderExtensionsImplementation project(modulePrefix + 'extension-vp9')
withDecoderExtensionsImplementation project(modulePrefix + 'extension-rtmp')
}
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
# 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 @@
android:largeHeap="true"
android:allowBackup="false"
android:requestLegacyExternalStorage="true"
android:name="com.google.android.exoplayer2.demo.DemoApplication"
tools:ignore="UnusedAttribute">
android:name="androidx.multidex.MultiDexApplication"
tools:targetApi="29">
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
android:configChanges="keyboardHidden"
......
......@@ -15,11 +15,12 @@
*/
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.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService;
......@@ -49,10 +50,9 @@ public class DemoDownloadService extends DownloadService {
protected DownloadManager getDownloadManager() {
// This will only happen once, because getDownloadManager is guaranteed to be called only once
// in the life cycle of the process.
DemoApplication application = (DemoApplication) getApplication();
DownloadManager downloadManager = application.getDownloadManager();
DownloadManager downloadManager = DemoUtil.getDownloadManager(/* context= */ this);
DownloadNotificationHelper downloadNotificationHelper =
application.getDownloadNotificationHelper();
DemoUtil.getDownloadNotificationHelper(/* context= */ this);
downloadManager.addListener(
new TerminalStateNotificationHelper(
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1));
......@@ -67,10 +67,13 @@ public class DemoDownloadService extends DownloadService {
@Override
@NonNull
protected Notification getForegroundNotification(@NonNull List<Download> downloads) {
return ((DemoApplication) getApplication())
.getDownloadNotificationHelper()
return DemoUtil.getDownloadNotificationHelper(/* context= */ this)
.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 {
}
@Override
public void onDownloadChanged(@NonNull DownloadManager manager, @NonNull Download download) {
public void onDownloadChanged(
DownloadManager downloadManager, Download download, @Nullable Exception finalException) {
Notification notification;
if (download.state == Download.STATE_COMPLETED) {
notification =
notificationHelper.buildDownloadCompletedNotification(
context,
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
} else if (download.state == Download.STATE_FAILED) {
notification =
notificationHelper.buildDownloadFailedNotification(
context,
R.drawable.ic_download_done,
/* contentIntent= */ null,
Util.fromUtf8Bytes(download.request.data));
......
......@@ -286,7 +286,7 @@ public final class TrackSelectionDialog extends DialogFragment {
private final class FragmentAdapter extends FragmentPagerAdapter {
public FragmentAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@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 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
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_height="match_parent"/>
android:layout_height="match_parent"
app:show_shuffle_button="true"
app:show_subtitle_button="true"/>
<LinearLayout
android:layout_width="match_parent"
......
......@@ -19,12 +19,4 @@
android:title="@string/prefer_extension_decoders"
android:checkable="true"
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>
......@@ -25,16 +25,10 @@
<string name="error_generic">Playback failed</string>
<string name="error_unrecognized_abr_algorithm">Unrecognized ABR algorithm</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_before_api_18">DRM 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_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_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 @@
<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_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
<string name="unsupported_ads_in_playlist">Playing without ads, as ads are not supported in playlists</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>
......@@ -69,8 +61,4 @@
<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>
......@@ -23,8 +23,4 @@
<item name="android:windowBackground">@android:color/black</item>
</style>
<style name="PlayerTheme.Spherical">
<item name="surface_type">spherical_gl_surface_view</item>
</style>
</resources>
......@@ -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
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
......@@ -28,6 +28,7 @@ import android.widget.Button;
import android.widget.GridLayout;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
......@@ -183,13 +184,12 @@ public final class MainActivity extends Activity {
ACTION_VIEW.equals(action)
? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI);
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
DrmSessionManager drmSessionManager;
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory();
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager =
......@@ -200,21 +200,19 @@ public final class MainActivity extends Activity {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
}
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(
this, Util.getUserAgent(this, getString(R.string.application_name)));
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
.createMediaSource(MediaItem.fromUri(uri));
} else {
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");
* you may not use this file except in compliance with the License.
......@@ -13,23 +13,7 @@
* See the License for the specific language governing permissions and
* 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.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() {}
}
import com.google.android.exoplayer2.util.NonNullApi;
......@@ -39,7 +39,7 @@ git clone https://github.com/google/cpu_features
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
git clone https://chromium.googlesource.com/codecs/libgav1
```
* Fetch Abseil:
......@@ -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`
gets from the libgav1 decoder:
* 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
`video_decoder_gl_surface_view`.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
of type `C.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
by default.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
* 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
`video_decoder_gl_surface_view`.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
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 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
GL rendering mode has better performance, so should be preferred
......
......@@ -11,22 +11,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
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'
externalNativeBuild {
cmake {
// Debug CMake build type causes video frames to drop,
......@@ -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
// failures if libgav1 hasn't been checked out according to the README and CMake
// isn't installed.
// Configure the native build only if libgav1 is present to avoid gradle sync
// failures if libgav1 hasn't been built according to the README instructions.
if (project.file('src/main/jni/libgav1').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
android.externalNativeBuild.cmake.version = '3.7.1+'
android.externalNativeBuild.cmake {
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 {
......
......@@ -20,6 +20,7 @@ import static java.lang.Runtime.getRuntime;
import android.view.Surface;
import androidx.annotation.Nullable;
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.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
......@@ -83,18 +84,9 @@ import java.nio.ByteBuffer;
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
protected VideoDecoderInputBuffer createInputBuffer() {
return new VideoDecoderInputBuffer();
return new VideoDecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
}
@Override
......@@ -128,7 +120,7 @@ import java.nio.ByteBuffer;
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
if (!decodeOnly) {
outputBuffer.colorInfo = inputBuffer.colorInfo;
outputBuffer.format = inputBuffer.format;
}
return null;
......@@ -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
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
*
......
......@@ -126,7 +126,7 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer {
|| !Gav1Library.isAvailable()) {
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_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
......
# libgav1JNI requires modern CMake.
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
# libgav1JNI requires C++11.
set(CMAKE_CXX_STANDARD 11)
project(libgav1JNI C CXX)
......@@ -21,24 +18,13 @@ if(build_type MATCHES "^rel")
endif()
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.
add_subdirectory("${cpu_features_root}"
"${cpu_features_build}"
add_subdirectory("${libgav1_jni_root}/cpu_features"
EXCLUDE_FROM_ALL)
# Build libgav1.
add_subdirectory("${libgav1_root}"
"${libgav1_build}"
add_subdirectory("${libgav1_jni_root}/libgav1"
EXCLUDE_FROM_ALL)
# Build libgav1JNI.
......@@ -58,7 +44,3 @@ target_link_libraries(gav1JNI
PRIVATE libgav1_static
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 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
api 'com.google.android.gms:play-services-cast-framework:18.1.0'
......@@ -36,7 +19,7 @@ dependencies {
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui')
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
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.cast;
import static java.lang.Math.min;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer;
......@@ -30,6 +32,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
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.Log;
import com.google.android.exoplayer2.util.MimeTypes;
......@@ -290,6 +293,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void addListener(EventListener listener) {
Assertions.checkNotNull(listener);
listeners.addIfAbsent(new ListenerHolder(listener));
}
......@@ -333,7 +337,7 @@ public final class CastPlayer extends BasePlayer {
&& toIndex <= currentTimeline.getWindowCount()
&& newIndex >= 0
&& newIndex < currentTimeline.getWindowCount());
newIndex = Math.min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
newIndex = min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
if (fromIndex == toIndex || fromIndex == newIndex) {
// Do nothing.
return;
......@@ -426,6 +430,9 @@ public final class CastPlayer extends BasePlayer {
return playWhenReady.value;
}
// We still call EventListener#onSeekProcessed() for backwards compatibility with listeners that
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override
public void seekTo(int windowIndex, long positionMs) {
MediaStatus mediaStatus = getMediaStatus();
......@@ -451,33 +458,17 @@ public final class CastPlayer extends BasePlayer {
flushNotifications();
}
/** @deprecated Use {@link #setPlaybackSpeed(float)} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
// Unsupported by the RemoteMediaClient API. Do nothing.
}
/** @deprecated Use {@link #getPlaybackSpeed()} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@Override
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
@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) {
playbackState = STATE_IDLE;
if (remoteMediaClient != null) {
......@@ -514,6 +505,12 @@ public final class CastPlayer extends BasePlayer {
}
@Override
@Nullable
public TrackSelector getTrackSelector() {
return null;
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) {
return;
......@@ -800,7 +797,7 @@ public final class CastPlayer extends BasePlayer {
}
return remoteMediaClient.queueLoad(
mediaQueueItems,
Math.min(startWindowIndex, mediaQueueItems.length - 1),
min(startWindowIndex, mediaQueueItems.length - 1),
getCastRepeatMode(repeatMode),
startPositionMs,
/* customData= */ null);
......@@ -874,7 +871,7 @@ public final class CastPlayer extends BasePlayer {
return;
}
if (this.remoteMediaClient != null) {
this.remoteMediaClient.removeListener(statusListener);
this.remoteMediaClient.unregisterCallback(statusListener);
this.remoteMediaClient.removeProgressListener(statusListener);
}
this.remoteMediaClient = remoteMediaClient;
......@@ -882,7 +879,7 @@ public final class CastPlayer extends BasePlayer {
if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionAvailable();
}
remoteMediaClient.addListener(statusListener);
remoteMediaClient.registerCallback(statusListener);
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
updateInternalStateAndNotifyIfChanged();
} else {
......@@ -996,10 +993,8 @@ public final class CastPlayer extends BasePlayer {
// Internal classes.
private final class StatusListener
implements RemoteMediaClient.Listener,
SessionManagerListener<CastSession>,
RemoteMediaClient.ProgressListener {
private final class StatusListener extends RemoteMediaClient.Callback
implements SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
// RemoteMediaClient.ProgressListener implementation.
......@@ -1008,7 +1003,7 @@ public final class CastPlayer extends BasePlayer {
lastReportedPositionMs = progressMs;
}
// RemoteMediaClient.Listener implementation.
// RemoteMediaClient.Callback implementation.
@Override
public void onStatusUpdated() {
......@@ -1085,6 +1080,9 @@ public final class CastPlayer extends BasePlayer {
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
public void onResult(MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode();
......
......@@ -15,10 +15,12 @@
*/
package com.google.android.exoplayer2.ext.cast;
import android.net.Uri;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
import java.util.Arrays;
......@@ -126,7 +128,7 @@ import java.util.Arrays;
boolean isDynamic = durationUs == C.TIME_UNSET;
return window.set(
/* uid= */ ids[windowIndex],
/* tag= */ ids[windowIndex],
/* mediaItem= */ new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(),
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
......
......@@ -59,8 +59,7 @@ public class CastPlayerTest {
private CastPlayer castPlayer;
@SuppressWarnings("deprecation")
private RemoteMediaClient.Listener remoteMediaClientListener;
private RemoteMediaClient.Callback remoteMediaClientCallback;
@Mock private RemoteMediaClient mockRemoteMediaClient;
@Mock private MediaStatus mockMediaStatus;
......@@ -76,7 +75,7 @@ public class CastPlayerTest {
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
setResultCallbackArgumentCaptor;
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
@Captor private ArgumentCaptor<RemoteMediaClient.Callback> callbackArgumentCaptor;
@Captor private ArgumentCaptor<MediaQueueItem[]> queueItemsArgumentCaptor;
......@@ -95,8 +94,8 @@ public class CastPlayerTest {
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
castPlayer = new CastPlayer(mockCastContext);
castPlayer.addListener(mockListener);
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
remoteMediaClientListener = listenerArgumentCaptor.getValue();
verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture());
remoteMediaClientCallback = callbackArgumentCaptor.getValue();
}
@SuppressWarnings("deprecation")
......@@ -113,7 +112,7 @@ public class CastPlayerTest {
.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
// There is a status update in the middle, which should be hidden by masking.
remoteMediaClientListener.onStatusUpdated();
remoteMediaClientCallback.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the remoteMediaClient has updated its state according to the play() call.
......@@ -169,7 +168,7 @@ public class CastPlayerTest {
public void playWhenReady_changesOnStatusUpdates() {
assertThat(castPlayer.getPlayWhenReady()).isFalse();
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
remoteMediaClientListener.onStatusUpdated();
remoteMediaClientCallback.onStatusUpdated();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
assertThat(castPlayer.getPlayWhenReady()).isTrue();
......@@ -187,7 +186,7 @@ public class CastPlayerTest {
// There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated();
remoteMediaClientCallback.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the mediaStatus now exposes the new repeat mode.
......@@ -209,7 +208,7 @@ public class CastPlayerTest {
// There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated();
remoteMediaClientCallback.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the repeat mode is ALL. The state should reflect that.
......@@ -224,7 +223,7 @@ public class CastPlayerTest {
public void repeatMode_changesOnStatusUpdates() {
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
remoteMediaClientListener.onStatusUpdated();
remoteMediaClientCallback.onStatusUpdated();
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
}
......@@ -494,6 +493,6 @@ public class CastPlayerTest {
castPlayer.addMediaItems(mediaItems);
// Call listener to update the timeline of the player.
remoteMediaClientListener.onQueueStatusUpdated();
remoteMediaClientCallback.onQueueStatusUpdated();
}
}
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.cast;
import static org.mockito.Mockito.when;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
......@@ -105,18 +107,18 @@ public class CastTimelineTrackerTest {
int[] itemIds, int currentItemId, long currentDurationMs) {
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
MediaStatus status = Mockito.mock(MediaStatus.class);
Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
when(status.getQueueItems()).thenReturn(Collections.emptyList());
when(remoteMediaClient.getMediaStatus()).thenReturn(status);
when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
when(status.getCurrentItemId()).thenReturn(currentItemId);
MediaQueue mediaQueue = mockMediaQueue(itemIds);
Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
return remoteMediaClient;
}
private static MediaQueue mockMediaQueue(int[] itemIds) {
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
when(mediaQueue.getItemIds()).thenReturn(itemIds);
return mediaQueue;
}
......
......@@ -11,29 +11,19 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions.unitTests.includeAndroidResources = true
}
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
api "com.google.android.gms:play-services-cronet:17.0.0"
implementation project(modulePrefix + 'library-core')
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.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'library')
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.cronet;
import static java.lang.Math.min;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.chromium.net.UploadDataProvider;
......@@ -40,7 +42,7 @@ import org.chromium.net.UploadDataSink;
@Override
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);
position += readLength;
uploadDataSink.onReadSucceeded(false);
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.cronet;
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
......@@ -50,14 +52,13 @@ public final class CronetDataSourceFactory extends BaseFactory {
private final HttpDataSource.Factory fallbackFactory;
/**
* Constructs a CronetDataSourceFactory.
* Creates an instance.
*
* <p>If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided
* fallback {@link HttpDataSource.Factory} 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 and disables
* cross-protocol redirects.
* {@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.
......@@ -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
* 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 and disables
* cross-protocol redirects.
* {@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 userAgent A user agent used to create a fallback HttpDataSource if needed.
*/
public CronetDataSourceFactory(
CronetEngineWrapper cronetEngineWrapper,
Executor executor,
String userAgent) {
CronetEngineWrapper cronetEngineWrapper, Executor executor, String userAgent) {
this(
cronetEngineWrapper,
executor,
......@@ -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
* DefaultHttpDataSourceFactory} will be used instead.
......@@ -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
* fallback {@link HttpDataSource.Factory} will be used instead.
......@@ -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
* fallback {@link HttpDataSource.Factory} 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 and disables
* cross-protocol redirects.
* {@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.
......@@ -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
* 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 and disables
* cross-protocol redirects.
* {@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.
......@@ -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
* DefaultHttpDataSourceFactory} will be used instead.
......@@ -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
* fallback {@link HttpDataSource.Factory} will be used instead.
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.cronet;
import static java.lang.Math.min;
import android.content.Context;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
......@@ -230,7 +232,7 @@ public final class CronetEngineWrapper {
}
String[] versionStringsLeft = Util.split(versionLeft, "\\.");
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++) {
if (!versionStringsLeft[i].equals(versionStringsRight[i])) {
try {
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.min;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
......@@ -64,13 +65,10 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadows.ShadowLooper;
/** Tests for {@link CronetDataSource}. */
@RunWith(AndroidJUnit4.class)
@LooperMode(Mode.PAUSED)
public final class CronetDataSourceTest {
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
......@@ -378,15 +376,18 @@ public final class CronetDataSourceTest {
}
@Test
public void requestOpenValidatesStatusCode() {
public void requestOpenPropagatesFailureResponseBody() throws Exception {
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 {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
assertThat(e).isInstanceOf(HttpDataSource.InvalidResponseCodeException.class);
fail("HttpDataSource.InvalidResponseCodeException expected");
} catch (HttpDataSource.InvalidResponseCodeException e) {
assertThat(e.responseBody).isEqualTo(buildTestDataArray(0, responseLength));
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never())
......@@ -1423,7 +1424,7 @@ public final class CronetDataSourceTest {
mockUrlRequest, testUrlResponseInfo);
} else {
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));
positionAndRemaining[0] += readLength;
positionAndRemaining[1] -= readLength;
......
......@@ -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][]
for more information).
In addition, it's necessary to build the extension's native components as
follows:
In addition, it's necessary to manually build the FFmpeg library, so that gradle
can bundle the FFmpeg binaries in the APK:
* Set the following shell variable:
```
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.
......@@ -41,6 +42,17 @@ NDK_PATH="<path to Android NDK>"
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
details of the available decoders, and which formats they support.
......@@ -48,22 +60,21 @@ HOST_PLATFORM="linux-x86_64"
ENABLED_DECODERS=(vorbis opus flac)
```
* Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build
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.
* Add a link to the FFmpeg source code in the FFmpeg extension `jni` directory.
```
cd "${FFMPEG_EXT_PATH}" && \
./build_ffmpeg.sh \
"${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
cd "${FFMPEG_EXT_PATH}/jni" && \
ln -s "$FFMPEG_PATH" ffmpeg
```
* Build the JNI native libraries, setting `APP_ABI` to include the architectures
built in the previous step. For example:
* Execute `build_ffmpeg.sh` to build FFmpeg 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}" && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" -j4
cd "${FFMPEG_EXT_PATH}/jni" && \
./build_ffmpeg.sh \
"${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
```
## Build instructions (Windows) ##
......
......@@ -11,29 +11,13 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
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'
}
sourceSets.main {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
}
testOptions.unitTests.includeAndroidResources = true
// Configure the native build only if ffmpeg is present to avoid gradle sync
// failures if ffmpeg hasn't been built according to the README instructions.
if (project.file('src/main/jni/ffmpeg').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
android.externalNativeBuild.cmake.version = '3.7.1+'
}
dependencies {
......
......@@ -52,10 +52,10 @@ import java.util.List;
private volatile int sampleRate;
public FfmpegAudioDecoder(
Format format,
int numInputBuffers,
int numOutputBuffers,
int initialInputBufferSize,
Format format,
boolean outputFloat)
throws FfmpegDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
......@@ -82,7 +82,9 @@ import java.util.List;
@Override
protected DecoderInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
return new DecoderInputBuffer(
DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT,
FfmpegLibrary.getInputBufferPaddingSize());
}
@Override
......
......@@ -15,6 +15,10 @@
*/
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 androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
......@@ -22,16 +26,17 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
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.DefaultAudioSink;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
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. */
public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioDecoder> {
private static final String TAG = "FfmpegAudioRenderer";
......@@ -40,10 +45,6 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
/** The default input buffer size. */
private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6;
private final boolean enableFloatOutput;
private @MonotonicNonNull FfmpegAudioDecoder decoder;
public FfmpegAudioRenderer() {
this(/* eventHandler= */ null, /* eventListener= */ null);
}
......@@ -63,8 +64,7 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
this(
eventHandler,
eventListener,
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors),
/* enableFloatOutput= */ false);
new DefaultAudioSink(/* audioCapabilities= */ null, audioProcessors));
}
/**
......@@ -74,21 +74,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
* 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 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(
@Nullable Handler eventHandler,
@Nullable AudioRendererEventListener eventListener,
AudioSink audioSink,
boolean enableFloatOutput) {
AudioSink audioSink) {
super(
eventHandler,
eventListener,
audioSink);
this.enableFloatOutput = enableFloatOutput;
}
@Override
......@@ -102,9 +96,11 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) {
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;
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
} else if (format.exoMediaCryptoType != null) {
return FORMAT_UNSUPPORTED_DRM;
} else {
return FORMAT_HANDLED;
......@@ -123,15 +119,15 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
TraceUtil.beginSection("createFfmpegAudioDecoder");
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
decoder =
FfmpegAudioDecoder decoder =
new FfmpegAudioDecoder(
NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format, shouldUseFloatOutput(format));
format, NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, shouldOutputFloat(format));
TraceUtil.endSection();
return decoder;
}
@Override
public Format getOutputFormat() {
public Format getOutputFormat(FfmpegAudioDecoder decoder) {
Assertions.checkNotNull(decoder);
return new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW)
......@@ -141,31 +137,36 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer {
.build();
}
private boolean isOutputSupported(Format inputFormat) {
return shouldUseFloatOutput(inputFormat)
|| supportsOutput(inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT);
/**
* Returns whether the renderer's {@link AudioSink} supports the PCM format that will be output
* 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) {
Assertions.checkNotNull(inputFormat.sampleMimeType);
if (!enableFloatOutput
|| !supportsOutput(
inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_FLOAT)) {
return false;
private boolean shouldOutputFloat(Format inputFormat) {
if (!sinkSupportsFormat(inputFormat, C.ENCODING_PCM_16BIT)) {
// We have no choice because the sink doesn't support 16-bit integer PCM.
return true;
}
switch (inputFormat.sampleMimeType) {
case MimeTypes.AUDIO_RAW:
// For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit.
return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT
|| inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT
|| inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT;
case MimeTypes.AUDIO_AC3:
// AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding.
return false;
@SinkFormatSupport
int formatSupport =
getSinkFormatSupport(
Util.getPcmFormat(
C.ENCODING_PCM_FLOAT, inputFormat.channelCount, inputFormat.sampleRate));
switch (formatSupport) {
case SINK_FORMAT_SUPPORTED_DIRECTLY:
// 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:
// For all other formats, assume that it's worth using 32-bit float encoding.
return true;
// Always prefer 16-bit PCM if the sink does not provide direct support for floating point.
return false;
}
}
}
......@@ -16,10 +16,12 @@
package com.google.android.exoplayer2.ext.ffmpeg;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Configures and queries the underlying native library.
......@@ -33,7 +35,10 @@ public final class FfmpegLibrary {
private static final String TAG = "FfmpegLibrary";
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() {}
......@@ -58,7 +63,27 @@ public final class FfmpegLibrary {
/** Returns the version of the underlying library if available, or null otherwise. */
@Nullable
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 {
}
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;
*/
public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
private static final String TAG = "FfmpegAudioRenderer";
private static final String TAG = "FfmpegVideoRenderer";
/**
* Creates a new instance.
......@@ -76,7 +76,7 @@ public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) {
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);
} else {
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[@]}"
do
COMMON_OPTIONS="${COMMON_OPTIONS} --enable-decoder=${decoder}"
done
cd "${FFMPEG_EXT_PATH}"
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg)
cd ffmpeg
git checkout release/4.2
cd "${FFMPEG_EXT_PATH}/jni/ffmpeg"
./configure \
--libdir=android-libs/armeabi-v7a \
--arch=arm \
......
......@@ -113,6 +113,10 @@ LIBRARY_FUNC(jstring, ffmpegGetVersion) {
return env->NewStringUTF(LIBAVCODEC_IDENT);
}
LIBRARY_FUNC(jint, ffmpegGetInputBufferPaddingSize) {
return (jint)AV_INPUT_BUFFER_PADDING_SIZE;
}
LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) {
return getCodecByName(env, codecName) != NULL;
}
......
......@@ -21,13 +21,22 @@ import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
import org.junit.Test;
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)
public final class DefaultRenderersFactoryTest {
@Test
public void createRenderers_instantiatesVpxRenderer() {
public void createRenderers_instantiatesFfmpegAudioRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
}
@Test
public void createRenderers_instantiatesFfmpegVideoRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegVideoRenderer.class, C.TRACK_TYPE_VIDEO);
}
}
......@@ -11,24 +11,9 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
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 {
main {
jniLibs.srcDir 'src/main/libs'
......@@ -36,8 +21,6 @@ android {
}
androidTest.assets.srcDir '../../testdata/src/test/assets/'
}
testOptions.unitTests.includeAndroidResources = true
}
dependencies {
......
......@@ -37,16 +37,16 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class FlacExtractorSeekTest {
private static final String TEST_FILE_SEEK_TABLE = "flac/bear.flac";
private static final String TEST_FILE_BINARY_SEARCH = "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_SEEK_TABLE = "media/flac/bear.flac";
private static final String TEST_FILE_BINARY_SEARCH = "media/flac/bear_one_metadata_block.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 FlacExtractor extractor = new FlacExtractor();
private FakeExtractorOutput extractorOutput = new FakeExtractorOutput();
private DefaultDataSource dataSource =
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent")
.createDataSource();
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()).createDataSource();
@Test
public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException {
......
......@@ -39,78 +39,80 @@ public class FlacExtractorTest {
@Test
public void sample() throws Exception {
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
public void sampleWithId3HeaderAndId3Enabled() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_with_id3.flac",
/* dumpFilesPrefix= */ "flac/bear_with_id3_enabled_raw");
/* file= */ "media/flac/bear_with_id3.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_enabled_raw");
}
@Test
public void sampleWithId3HeaderAndId3Disabled() throws Exception {
ExtractorAsserts.assertAllBehaviors(
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
/* file= */ "flac/bear_with_id3.flac",
/* dumpFilesPrefix= */ "flac/bear_with_id3_disabled_raw");
/* file= */ "media/flac/bear_with_id3.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_id3_disabled_raw");
}
@Test
public void sampleUnseekable() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_no_seek_table_no_num_samples.flac",
/* dumpFilesPrefix= */ "flac/bear_no_seek_table_no_num_samples_raw");
/* file= */ "media/flac/bear_no_seek_table_no_num_samples.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_seek_table_no_num_samples_raw");
}
@Test
public void sampleWithVorbisComments() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_with_vorbis_comments.flac",
/* dumpFilesPrefix= */ "flac/bear_with_vorbis_comments_raw");
/* file= */ "media/flac/bear_with_vorbis_comments.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_vorbis_comments_raw");
}
@Test
public void sampleWithPicture() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_with_picture.flac",
/* dumpFilesPrefix= */ "flac/bear_with_picture_raw");
/* file= */ "media/flac/bear_with_picture.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_with_picture_raw");
}
@Test
public void oneMetadataBlock() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_one_metadata_block.flac",
/* dumpFilesPrefix= */ "flac/bear_one_metadata_block_raw");
/* file= */ "media/flac/bear_one_metadata_block.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_one_metadata_block_raw");
}
@Test
public void noMinMaxFrameSize() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_no_min_max_frame_size.flac",
/* dumpFilesPrefix= */ "flac/bear_no_min_max_frame_size_raw");
/* file= */ "media/flac/bear_no_min_max_frame_size.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_min_max_frame_size_raw");
}
@Test
public void noNumSamples() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_no_num_samples.flac",
/* dumpFilesPrefix= */ "flac/bear_no_num_samples_raw");
/* file= */ "media/flac/bear_no_num_samples.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_no_num_samples_raw");
}
@Test
public void uncommonSampleRate() throws Exception {
ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_uncommon_sample_rate.flac",
/* dumpFilesPrefix= */ "flac/bear_uncommon_sample_rate_raw");
/* file= */ "media/flac/bear_uncommon_sample_rate.flac",
/* dumpFilesPrefix= */ "extractordumps/flac/bear_uncommon_sample_rate_raw");
}
}
......@@ -25,6 +25,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioSink;
......@@ -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.ProgressiveMediaSource;
import com.google.android.exoplayer2.testutil.CapturingAudioSink;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.junit.Before;
import org.junit.Test;
......@@ -69,7 +71,7 @@ public class FlacPlaybackTest {
TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(
Uri.parse("asset:///" + fileName),
Uri.parse("asset:///media/" + fileName),
ApplicationProvider.getApplicationContext(),
audioSink);
Thread thread = new Thread(testPlaybackRunnable);
......@@ -79,8 +81,10 @@ public class FlacPlaybackTest {
throw testPlaybackRunnable.playbackException;
}
audioSink.assertOutput(
ApplicationProvider.getApplicationContext(), fileName + ".audiosink.dump");
DumpFileAsserts.assertOutput(
ApplicationProvider.getApplicationContext(),
audioSink,
"audiosinkdumps/" + fileName + ".audiosink.dump");
}
private static class TestPlaybackRunnable implements Player.EventListener, Runnable {
......@@ -107,9 +111,8 @@ public class FlacPlaybackTest {
player.addListener(this);
MediaSource mediaSource =
new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"),
MatroskaExtractor.FACTORY)
.createMediaSource(uri);
new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY)
.createMediaSource(MediaItem.fromUri(uri));
player.setMediaSource(mediaSource);
player.prepare();
player.play();
......
......@@ -15,6 +15,8 @@
*/
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.ExtractorInput;
import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
......@@ -74,7 +76,7 @@ import java.nio.ByteBuffer;
/* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(
/* minimumSearchRange= */ max(
FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni);
}
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.flac;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
......@@ -118,7 +120,7 @@ import java.nio.ByteBuffer;
public int read(ByteBuffer target) throws IOException {
int byteCount = target.remaining();
if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining());
byteCount = min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount);
target.put(byteBufferData);
......@@ -126,7 +128,7 @@ import java.nio.ByteBuffer;
} else if (extractorInput != null) {
ExtractorInput extractorInput = this.extractorInput;
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);
if (read < 4) {
// 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 {
/** Factory that returns one extractor which is a {@link 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
* #FLAG_DISABLE_ID3_METADATA}.
......@@ -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
* 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 boolean id3MetadataDisabled;
......@@ -203,7 +210,7 @@ public final class FlacExtractor implements Extractor {
if (this.streamMetadata == null) {
this.streamMetadata = streamMetadata;
outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.getData()));
binarySearchSeeker =
outputSeekMap(
flacDecoderJni,
......
......@@ -25,26 +25,24 @@ import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
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.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** 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 int NUM_BUFFERS = 16;
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
public LibflacAudioRenderer() {
this(/* eventHandler= */ null, /* eventListener= */ null);
}
/**
* Creates an instance.
*
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
......@@ -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
* 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 {
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
}
// Compute the PCM encoding that the FLAC decoder will output.
@C.PcmEncoding int pcmEncoding;
// Compute the format that the FLAC decoder will output.
Format outputFormat;
if (format.initializationData.isEmpty()) {
// 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
// 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.
pcmEncoding = C.ENCODING_PCM_16BIT;
outputFormat =
Util.getPcmFormat(C.ENCODING_PCM_16BIT, format.channelCount, format.sampleRate);
} else {
int streamMetadataOffset =
FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE;
FlacStreamMetadata streamMetadata =
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;
} else if (format.drmInitData != null && format.exoMediaCryptoType == null) {
} else if (format.exoMediaCryptoType != null) {
return FORMAT_UNSUPPORTED_DRM;
} else {
return FORMAT_HANDLED;
......@@ -115,19 +116,19 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer {
TraceUtil.beginSection("createFlacDecoder");
FlacDecoder decoder =
new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
streamMetadata = decoder.getStreamMetadata();
TraceUtil.endSection();
return decoder;
}
@Override
protected Format getOutputFormat() {
Assertions.checkNotNull(streamMetadata);
return new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW)
.setChannelCount(streamMetadata.channels)
.setSampleRate(streamMetadata.sampleRate)
.setPcmEncoding(Util.getPcmEncoding(streamMetadata.bitsPerSample))
.build();
protected Format getOutputFormat(FlacDecoder decoder) {
return getOutputFormat(decoder.getStreamMetadata());
}
private static Format getOutputFormat(FlacStreamMetadata streamMetadata) {
return Util.getPcmFormat(
Util.getPcmEncoding(streamMetadata.bitsPerSample),
streamMetadata.channels,
streamMetadata.sampleRate);
}
}
......@@ -26,7 +26,7 @@ import org.junit.runner.RunWith;
public final class DefaultRenderersFactoryTest {
@Test
public void createRenderers_instantiatesVpxRenderer() {
public void createRenderers_instantiatesFlacRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO);
}
......
......@@ -11,24 +11,9 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android {
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
}
android.defaultConfig.minSdkVersion 19
dependencies {
implementation project(modulePrefix + 'library-core')
......
......@@ -26,35 +26,29 @@ locally. Instructions for doing this can be found in ExoPlayer's
## Using the extension ##
To play ads alongside a single-window content `MediaSource`, prepare the player
with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content
`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag
URI from your ad campaign when creating the `ImaAdsLoader`. The IMA
documentation includes some [sample ad tags][] for testing. Note that the IMA
To use the extension, follow the instructions on the
[Ad insertion page](https://exoplayer.dev/ad-insertion.html#declarative-ad-support)
of the developer guide. The `AdsLoaderProvider` passed to the player's
`DefaultMediaSourceFactory` should return an `ImaAdsLoader`. Note that the IMA
extension only supports players which are accessed on the application's main
thread.
Resuming the player after entering the background requires some special handling
when playing ads. The player and its media source are released on entering the
background, and are recreated when the player returns to the foreground. When
playing ads it is necessary to persist ad playback state while in the background
by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of
the same content/ads by passing it in when constructing the new
`AdsMediaSource`. It is also important to persist the player position when
background, and are recreated when returning to the foreground. When playing ads
it is necessary to persist ad playback state while in the background by keeping
a reference to the `ImaAdsLoader`. When re-entering the foreground, pass the
same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called
to restore the state. It is also important to persist the player position when
entering the background by storing the value of `player.getContentPosition()`.
On returning to the foreground, seek to that position before preparing the new
player instance. Finally, it is important to call `ImaAdsLoader.release()` when
playback of the content/ads has finished and will not be resumed.
playback has finished and will not be resumed.
You can try the IMA extension in the ExoPlayer demo app. To do this you must
select and build one of the `withExtensions` build variants of the demo app in
Android Studio. You can find IMA test content in the "IMA sample ad tags"
section of the app. The demo app's `PlayerActivity` also shows how to persist
the `ImaAdsLoader` instance and the player position when backgrounded during ad
playback.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags
You can try the IMA extension in the ExoPlayer demo app, which has test content
in the "IMA sample ad tags" section of the sample chooser. The demo app's
`PlayerActivity` also shows how to persist the `ImaAdsLoader` instance and the
player position when backgrounded during ad playback.
## Links ##
......
......@@ -11,22 +11,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
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'
// Enable multidex for androidTests.
multiDexEnabled true
}
......@@ -34,22 +22,42 @@ android {
sourceSets {
androidTest.assets.srcDir '../../testdata/src/test/assets/'
}
testOptions.unitTests.includeAndroidResources = true
}
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 'androidx.annotation:annotation:' + androidxAnnotationVersion
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
androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
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
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
}
......
......@@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.net.Uri;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
......@@ -39,6 +38,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
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.AdsMediaSource;
import com.google.android.exoplayer2.testutil.ActionSchedule;
......@@ -49,7 +49,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
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.Arrays;
import java.util.List;
......@@ -234,29 +234,26 @@ public final class ImaPlaybackTest {
@Override
protected MediaSource buildSource(
HostActivity host,
String userAgent,
DrmSessionManager drmSessionManager,
FrameLayout overlayFrameLayout) {
Context context = host.getApplicationContext();
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(
context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName()));
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context);
MediaSource contentMediaSource =
DefaultMediaSourceFactory.newInstance(context)
.createMediaSource(MediaItem.fromUri(contentUri));
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
return new AdsMediaSource(
contentMediaSource,
dataSourceFactory,
Assertions.checkNotNull(imaAdsLoader),
new AdViewProvider() {
@Override
public ViewGroup getAdViewGroup() {
return overlayFrameLayout;
}
@Override
public View[] getAdOverlayViews() {
return new View[0];
public ImmutableList<AdsLoader.OverlayInfo> getAdOverlayInfos() {
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 #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][]
instead.**
**This extension is deprecated. Use the [WorkManager extension][] instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[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
## Getting the extension ##
......
......@@ -13,24 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
testOptions.unitTests.includeAndroidResources = true
}
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
implementation project(modulePrefix + 'library-core')
......
......@@ -60,11 +60,15 @@ import com.google.android.exoplayer2.util.Util;
@Deprecated
public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false;
private static final String TAG = "JobDispatcherScheduler";
private static final String KEY_SERVICE_ACTION = "service_action";
private static final String KEY_SERVICE_PACKAGE = "service_package";
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 FirebaseJobDispatcher jobDispatcher;
......@@ -85,35 +89,44 @@ public final class JobDispatcherScheduler implements Scheduler {
public boolean schedule(Requirements requirements, String servicePackage, String serviceAction) {
Job job = buildJob(jobDispatcher, requirements, jobTag, servicePackage, serviceAction);
int result = jobDispatcher.schedule(job);
logd("Scheduling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
}
@Override
public boolean cancel() {
int result = jobDispatcher.cancel(jobTag);
logd("Canceling job: " + jobTag + " result: " + result);
return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
}
@Override
public Requirements getSupportedRequirements(Requirements requirements) {
return requirements.filterRequirements(SUPPORTED_REQUIREMENTS);
}
private static Job buildJob(
FirebaseJobDispatcher dispatcher,
Requirements requirements,
String tag,
String servicePackage,
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 =
dispatcher
.newJobBuilder()
.setService(JobDispatcherSchedulerService.class) // the JobService that will be called
.setTag(tag);
if (requirements.isUnmeteredNetworkRequired()) {
builder.addConstraint(Constraint.ON_UNMETERED_NETWORK);
} else if (requirements.isNetworkRequired()) {
builder.addConstraint(Constraint.ON_ANY_NETWORK);
}
if (requirements.isIdleRequired()) {
builder.addConstraint(Constraint.DEVICE_IDLE);
}
......@@ -131,31 +144,20 @@ public final class JobDispatcherScheduler implements Scheduler {
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. */
public static final class JobDispatcherSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
logd("JobDispatcherSchedulerService is started");
Bundle extras = params.getExtras();
Assertions.checkNotNull(extras, "Service started without extras.");
Bundle extras = Assertions.checkNotNull(params.getExtras());
Requirements requirements = new Requirements(extras.getInt(KEY_REQUIREMENTS));
if (requirements.checkRequirements(this)) {
logd("Requirements are met");
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
Assertions.checkNotNull(serviceAction, "Service action missing.");
Assertions.checkNotNull(servicePackage, "Service package missing.");
int notMetRequirements = requirements.getNotMetRequirements(this);
if (notMetRequirements == 0) {
String serviceAction = Assertions.checkNotNull(extras.getString(KEY_SERVICE_ACTION));
String servicePackage = Assertions.checkNotNull(extras.getString(KEY_SERVICE_PACKAGE));
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(this, intent);
} else {
logd("Requirements are not met");
Log.w(TAG, "Requirements not met: " + notMetRequirements);
jobFinished(params, /* needsReschedule */ true);
}
return false;
......
......@@ -11,24 +11,9 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.library'
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android {
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
}
android.defaultConfig.minSdkVersion 17
dependencies {
implementation project(modulePrefix + 'library-core')
......
......@@ -72,7 +72,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
this.context = context;
this.player = player;
this.updatePeriodMs = updatePeriodMs;
handler = Util.createHandler();
handler = Util.createHandlerForCurrentOrMainLooper();
componentListener = new ComponentListener();
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