Commit c17d1467 by BAI Yanning Committed by GitHub

Merge pull request #5 from google/dev-v2

Merge from google/ExoPlayer/dev-v2
parents 20aeb0c8 24a19264
Showing with 2286 additions and 1050 deletions

Too many changes to show.

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

---
name: Bug report
about: Issue template for a bug report.
title: ''
labels: bug, needs triage
assignees: ''
---
Before filing a bug:
-----------------------
- Search existing issues, including issues that are closed:
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our developer website, which can be found at https://exoplayer.dev/.
It provides detailed information about supported formats and devices.
- Learn how to create useful log output by using the EventLogger:
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
- Rule out issues in your own code. A good way to do this is to try and
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
demo app can be found here:
http://exoplayer.dev/demo-application.html.
When reporting a bug:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### [REQUIRED] Issue description
Describe the issue in detail, including observed and expected behavior.
### [REQUIRED] Reproduction steps
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app
or a small sample app that you’re able to share as source code on GitHub.
### [REQUIRED] Link to test content
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
media that reproduces the issue. If you don't wish to post it publicly, please
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
in the format "Issue #1234", where "#1234" should be replaced with your issue
number. Provide all the metadata we'd need to play the content like drm license
urls or similar. If the content is accessible only in certain countries or
regions, please say so.
### [REQUIRED] A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234", where "#1234" should be replaced with your issue number.
### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".
### [REQUIRED] Device(s) and version(s) of Android being used
Specify the devices and versions of Android on which the issue can be
reproduced, and how easily it reproduces. If possible, please test on multiple
devices and Android versions.
---
name: Feature request
about: Issue template for a feature request.
title: ''
labels: enhancement, needs triage
assignees: ''
---
Before filing a feature request:
-----------------------
- Search existing open issues, specifically with the label ‘enhancement’:
https://github.com/google/ExoPlayer/labels/enhancement
- Search existing pull requests: https://github.com/google/ExoPlayer/pulls
When filing a feature request:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### [REQUIRED] Use case description
Describe the use case or problem you are trying to solve in detail. If there are
any standards or specifications involved, please provide the relevant details.
### Proposed solution
A clear and concise description of your proposed solution, if you have one.
### Alternatives considered
A clear and concise description of any alternative solutions you considered,
if applicable.
---
name: Question
about: Issue template for a question.
title: ''
labels: question, needs triage
assignees: ''
---
Before filing a question:
-----------------------
- This issue tracker is intended ExoPlayer specific questions. If you're asking
a general Android development question, please do so on Stack Overflow.
- Search existing issues, including issues that are closed. It’s often the
quickest way to get an answer!
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our developer website, which can be found at https://exoplayer.dev/.
It provides detailed information about supported formats, devices as well as
information about how to use the ExoPlayer library.
- The ExoPlayer library Javadoc can be found at
https://exoplayer.dev/doc/reference/
When filing a question:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### [REQUIRED] Searched documentation and issues
Tell us where you’ve already looked for an answer to your question. It’s
important for us to know this so that we can improve our documentation.
### [REQUIRED] Question
Describe your question in detail.
### A full bug report captured from the device
In case your question refers to a problem you are seeing in your app, capture a
full bug report using "adb bugreport". Please attach the captured bug report as
a file. If you don't wish to post it publicly, please submit the issue, then
email the bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234", where "#1234" should be replaced with your issue number.
### Link to test content
In case your question is related to a piece of media, which you are trying to
play, please provide a JSON snippet for the demo app’s media.exolist.json file,
or a link to media that reproduces the issue. If you don't wish to post it
publicly, please submit the issue, then email the link to
dev.exoplayer@gmail.com using a subject in the format "Issue #1234", where
"#1234" should be replaced with your issue number. Provide all the metadata we'd
need to play the content like drm license urls or similar. If the content is
accessible only in certain countries or regions, please say so.
...@@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx ...@@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
...@@ -71,7 +74,3 @@ extensions/cronet/jniLibs/* ...@@ -71,7 +74,3 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app
...@@ -12,13 +12,14 @@ libs ...@@ -12,13 +12,14 @@ libs
obj obj
lint.xml lint.xml
# IntelliJ IDEA # IntelliJ IDEA & Android Studio
.idea .idea
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
classes classes
gen-external-apklibs gen-external-apklibs
*.li
# Eclipse # Eclipse
.project .project
...@@ -61,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx ...@@ -61,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
...@@ -75,7 +79,3 @@ extensions/cronet/jniLibs/* ...@@ -75,7 +79,3 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md !extensions/cronet/jniLibs/README.md
extensions/cronet/libs/* extensions/cronet/libs/*
!extensions/cronet/libs/README.md !extensions/cronet/libs/README.md
# Cast receiver
cast_receiver_app/external-js
cast_receiver_app/bazel-cast_receiver_app
Before filing an issue:
-----------------------
- Search existing issues, including issues that are closed.
- Consult our FAQs, supported devices and supported formats pages. These can be
found at https://google.github.io/ExoPlayer/.
- Rule out issues in your own code. A good way to do this is to try and
reproduce the issue in the ExoPlayer demo app.
- This issue tracker is intended for bugs, feature requests and ExoPlayer
specific questions. If you're asking a general Android development question,
please do so on Stack Overflow.
When reporting a bug:
-----------------------
Fill out the sections below, leaving the headers but replacing the content. If
you're unable to provide certain information, please explain why in the relevant
section. We may close issues if they do not include sufficient information.
### Issue description
Describe the issue in detail, including observed and expected behavior.
### Reproduction steps
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app.
### Link to test content
Provide a link to media that reproduces the issue. If you don't wish to post it
publicly, please submit the issue, then email the link to
dev.exoplayer@gmail.com using a subject in the format "Issue #1234".
### Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest".
### Device(s) and version(s) of Android being used
Specify the devices and versions of Android on which the issue can be
reproduced, and how easily it reproduces. If possible, please test on multiple
devices and Android versions.
### A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
log snippet is NOT sufficient. Please attach the captured bug report as a file.
If you don't wish to post it publicly, please submit the issue, then email the
bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234".
...@@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates. ...@@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates.
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer * Follow our [developer blog][] to keep up to date with the latest ExoPlayer
developments! developments!
[developer guide]: https://google.github.io/ExoPlayer/guide.html [developer guide]: https://exoplayer.dev/guide.html
[class reference]: https://google.github.io/ExoPlayer/doc/reference [class reference]: https://exoplayer.dev/doc/reference
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md [release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
[developer blog]: https://medium.com/google-exoplayer [developer blog]: https://medium.com/google-exoplayer
...@@ -95,20 +95,6 @@ compileOptions { ...@@ -95,20 +95,6 @@ compileOptions {
} }
``` ```
Note that if you want to use Java 8 features in your own code, the following
additional options need to be set:
```gradle
// For Java compilers:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin compilers:
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
```
### Locally ### ### Locally ###
Cloning the repository and depending on the modules locally is required when Cloning the repository and depending on the modules locally is required when
...@@ -121,6 +107,7 @@ branch: ...@@ -121,6 +107,7 @@ branch:
```sh ```sh
git clone https://github.com/google/ExoPlayer.git git clone https://github.com/google/ExoPlayer.git
cd ExoPlayer
git checkout release-v2 git checkout release-v2
``` ```
......
...@@ -17,17 +17,9 @@ buildscript { ...@@ -17,17 +17,9 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.4' classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.novoda:bintray-release:0.8.1' classpath 'com.novoda:bintray-release:0.9.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'
}
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
configurations.all {
resolutionStrategy {
force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
}
} }
} }
allprojects { allprojects {
...@@ -36,7 +28,7 @@ allprojects { ...@@ -36,7 +28,7 @@ allprojects {
jcenter() jcenter()
} }
project.ext { project.ext {
exoplayerPublishEnabled = true exoplayerPublishEnabled = false
} }
if (it.hasProperty('externalBuildDir')) { if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) { if (!new File(externalBuildDir).isAbsolute()) {
...@@ -44,6 +36,7 @@ allprojects { ...@@ -44,6 +36,7 @@ allprojects {
} }
buildDir = "${externalBuildDir}/${project.name}" buildDir = "${externalBuildDir}/${project.name}"
} }
group = 'com.google.android.exoplayer'
} }
apply from: 'javadoc_combined.gradle' apply from: 'javadoc_combined.gradle'
...@@ -13,26 +13,29 @@ ...@@ -13,26 +13,29 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.9.4' releaseVersion = '2.11.1'
releaseVersionCode = 2009004 releaseVersionCode = 2011001
// Important: ExoPlayer specifies a minSdkVersion of 14 because various minSdkVersion = 16
// components provided by the library may be of use on older devices. appTargetSdkVersion = 29
// However, please note that the core media playback functionality provided targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
// by the library requires API level 16 or greater. compileSdkVersion = 29
minSdkVersion = 14 dexmakerVersion = '2.21.0'
targetSdkVersion = 28 junitVersion = '4.13-rc-2'
compileSdkVersion = 28 guavaVersion = '23.5-android'
buildToolsVersion = '28.0.2' mockitoVersion = '2.25.0'
testSupportLibraryVersion = '0.5' robolectricVersion = '4.3.1'
supportLibraryVersion = '27.1.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
junitVersion = '4.12'
truthVersion = '0.39'
robolectricVersion = '3.7.1'
autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0' checkerframeworkVersion = '2.5.0'
testRunnerVersion = '1.1.0-alpha3' jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.3.31'
androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1'
androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0'
androidxTestRulesVersion = '1.2.0'
truthVersion = '1.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix
......
...@@ -24,7 +24,7 @@ include modulePrefix + 'library-hls' ...@@ -24,7 +24,7 @@ include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-ui' include modulePrefix + 'library-ui'
include modulePrefix + 'testutils' include modulePrefix + 'testutils'
include modulePrefix + 'testutils-robolectric' include modulePrefix + 'extension-av1'
include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac' include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-gvr'
...@@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9' ...@@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher' include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
...@@ -46,7 +47,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl ...@@ -46,7 +47,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric') project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
...@@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio ...@@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp')
project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback')
project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher') project(modulePrefix + 'extension-jobdispatcher').projectDir = new File(rootDir, 'extensions/jobdispatcher')
project(modulePrefix + 'extension-workmanager').projectDir = new File(rootDir, 'extensions/workmanager')
...@@ -16,7 +16,6 @@ apply plugin: 'com.android.application' ...@@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
...@@ -26,8 +25,8 @@ android { ...@@ -26,8 +25,8 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
...@@ -45,20 +44,9 @@ android { ...@@ -45,20 +44,9 @@ android {
} }
lintOptions { lintOptions {
// The demo app does not have translations. // The demo app isn't indexed and doesn't have translations.
disable 'MissingTranslation' disable 'GoogleAppIndexingWarning','MissingTranslation'
} }
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
} }
dependencies { dependencies {
...@@ -68,9 +56,9 @@ dependencies { ...@@ -68,9 +56,9 @@ dependencies {
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast') implementation project(modulePrefix + 'extension-cast')
implementation 'com.android.support:support-v4:' + supportLibraryVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion implementation 'com.google.android.material:material:1.0.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
# Proguard rules specific to the Cast demo app. # Proguard rules specific to the Cast demo app.
# Accessed via menu.xml # Accessed via menu.xml
-keep class android.support.v7.app.MediaRouteActionProvider { -keep class androidx.mediarouter.app.MediaRouteActionProvider {
*; *;
} }
...@@ -17,13 +17,15 @@ ...@@ -17,13 +17,15 @@
package="com.google.android.exoplayer2.castdemo"> package="com.google.android.exoplayer2.castdemo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/> <uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="${castOptionsProvider}" /> android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity" <activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
......
...@@ -16,98 +16,87 @@ ...@@ -16,98 +16,87 @@
package com.google.android.exoplayer2.castdemo; package com.google.android.exoplayer2.castdemo;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
/** Utility methods and constants for the Cast demo application. */ /** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil { /* package */ final class DemoUtil {
/** Represents a media sample. */
public static final class Sample {
/** The uri of the media content. */
public final String uri;
/** The name of the sample. */
public final String name;
/** The mime type of the sample media content. */
public final String mimeType;
/**
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
* DRM-protected.
*/
@Nullable public final UUID drmSchemeUuid;
/**
* The url from which players should obtain DRM licenses, or null if the content is not
* DRM-protected.
*/
@Nullable public final Uri licenseServerUri;
/**
* @param uri See {@link #uri}.
* @param name See {@link #name}.
* @param mimeType See {@link #mimeType}.
*/
public Sample(String uri, String name, String mimeType) {
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
}
public Sample(
String uri,
String name,
String mimeType,
@Nullable UUID drmSchemeUuid,
@Nullable String licenseServerUriString) {
this.uri = uri;
this.name = name;
this.mimeType = mimeType;
this.drmSchemeUuid = drmSchemeUuid;
this.licenseServerUri =
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
}
@Override
public String toString() {
return name;
}
}
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */ /** The list of samples available in the cast demo app. */
public static final List<Sample> SAMPLES; public static final List<MediaItem> SAMPLES;
static { static {
// App samples. ArrayList<MediaItem> samples = new ArrayList<>();
ArrayList<Sample> samples = new ArrayList<>();
// Clear content.
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
"Clear DASH: Tears", .setTitle("Clear DASH: Tears")
MIME_TYPE_DASH)); .setMimeType(MIME_TYPE_DASH)
.build());
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
+ "hls/TearsOfSteel.m3u8", .setTitle("Clear HLS: Angel one")
"Clear HLS: Tears of Steel", .setMimeType(MIME_TYPE_HLS)
MIME_TYPE_HLS)); .build());
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3" .setUri("https://html5demos.com/assets/dizzy.mp4")
+ "/bipbop_4x3_variant.m3u8", .setTitle("Clear MP4: Dizzy")
"Clear HLS: Basic 4x3", .setMimeType(MIME_TYPE_VIDEO_MP4)
MIME_TYPE_HLS)); .build());
// DRM content.
samples.add(
new MediaItem.Builder()
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
.setTitle("Widevine DASH cenc: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); .setUri(
Uri.parse(
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
.setTitle("Widevine DASH cbc1: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
samples.add(
new MediaItem.Builder()
.setUri(
Uri.parse(
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
.setTitle("Widevine DASH cbcs: Tears")
.setMimeType(MIME_TYPE_DASH)
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
Collections.emptyMap()))
.build());
SAMPLES = Collections.unmodifiableList(samples); SAMPLES = Collections.unmodifiableList(samples);
} }
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24.0dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
android:width="24.0dp" >
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1h0c-0.55,0 -1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1v0c0,-0.55 0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v5h5c0.55,0 1,0.45 1,1v0C19,12.55 18.55,13 18,13z"/>
</vector>
...@@ -13,17 +13,10 @@ ...@@ -13,17 +13,10 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="20sp"
android:gravity="center" android:gravity="center"
android:textSize="20sp"
android:text="@string/cast_context_error"/> android:text="@string/cast_context_error"/>
</LinearLayout>
...@@ -19,34 +19,42 @@ ...@@ -19,34 +19,42 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"> android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view" <com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="12" android:layout_weight="1"
android:background="@android:color/black"
app:repeat_toggle_modes="all|one"/> app:repeat_toggle_modes="all|one"/>
<RelativeLayout android:layout_width="match_parent" <RelativeLayout android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="12"> android:layout_weight="1">
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
<androidx.recyclerview.widget.RecyclerView android:id="@+id/sample_list"
android:choiceMode="singleChoice" android:choiceMode="singleChoice"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" android:scrollbars="vertical"
android:fadeScrollbars="false"/> android:fadeScrollbars="false"/>
<ImageButton android:id="@+id/add_sample_button"
android:background="@drawable/ic_add_circle_white_24dp" <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/add_sample_button"
android:src="@drawable/ic_plus"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:padding="30dp"/> android:layout_margin="16dp"
android:contentDescription="@string/add_samples"/>
</RelativeLayout> </RelativeLayout>
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view" <com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_weight="2"
android:visibility="gone" android:visibility="gone"
app:repeat_toggle_modes="all|one" app:repeat_toggle_modes="all|one"
app:show_timeout="-1"/> app:show_timeout="-1"/>
</LinearLayout> </LinearLayout>
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ListView android:id="@+id/sample_list" <ListView android:id="@+id/sample_list"
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
<item <item
android:id="@+id/media_route_menu_item" android:id="@+id/media_route_menu_item"
android:title="@string/media_route_menu_title" android:title="@string/media_route_menu_title"
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider" app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" /> app:showAsAction="always" />
</menu> </menu>
...@@ -20,8 +20,12 @@ ...@@ -20,8 +20,12 @@
<string name="media_route_menu_title">Cast</string> <string name="media_route_menu_title">Cast</string>
<string name="sample_list_dialog_title">Add samples</string> <string name="add_samples">Add samples</string>
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string> <string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
<string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
</resources> </resources>
# IMA demo application #
This folder contains a demo application that showcases ExoPlayer integration
with the IMA SDK.
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.imademo;
import android.app.Activity;
import android.os.Bundle;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ui.PlayerView;
/**
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
* {@link PlayerManager}, which this class instantiates.
*/
public final class MainActivity extends Activity {
private PlayerView playerView;
private PlayerManager player;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
playerView = findViewById(R.id.player_view);
player = new PlayerManager(this);
}
@Override
public void onResume() {
super.onResume();
player.init(this, playerView);
}
@Override
public void onPause() {
super.onPause();
player.reset();
}
@Override
public void onDestroy() {
player.release();
super.onDestroy();
}
}
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.imademo;
import android.content.Context;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
private final ImaAdsLoader adsLoader;
private final DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private long contentPosition;
public PlayerManager(Context context) {
String adTag = context.getString(R.string.ad_tag_url);
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
dataSourceFactory =
new DefaultDataSourceFactory(
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
}
public void init(Context context, PlayerView playerView) {
// Create a default track selector.
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
// Create a player instance.
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
// Bind the player to the view.
playerView.setPlayer(player);
// This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url);
MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
// Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds =
new AdsMediaSource(
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
playerView.getOverlayFrameLayout());
// Prepare the player with the source.
player.seekTo(contentPosition);
player.prepare(mediaSourceWithAds);
player.setPlayWhenReady(true);
}
public void reset() {
if (player != null) {
contentPosition = player.getContentPosition();
player.release();
player = null;
}
}
public void release() {
if (player != null) {
player.release();
player = null;
}
adsLoader.release();
}
// AdsMediaSource.MediaSourceFactory implementation.
@Override
public MediaSource createMediaSource(Uri uri) {
return buildMediaSource(uri);
}
@Override
public int[] getSupportedTypes() {
// IMA does not support Smooth Streaming ads.
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
}
// Internal methods.
private MediaSource buildMediaSource(Uri uri) {
@ContentType int type = Util.inferContentType(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
...@@ -16,7 +16,6 @@ apply plugin: 'com.android.application' ...@@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
...@@ -26,8 +25,8 @@ android { ...@@ -26,8 +25,8 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
...@@ -45,8 +44,9 @@ android { ...@@ -45,8 +44,9 @@ android {
} }
lintOptions { lintOptions {
// The demo app does not have translations. // The demo app isn't indexed, doesn't have translations, and has a
disable 'MissingTranslation' // banner for AndroidTV that's only in xhdpi density.
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
} }
flavorDimensions "extensions" flavorDimensions "extensions"
...@@ -62,12 +62,15 @@ android { ...@@ -62,12 +62,15 @@ android {
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'com.google.android.material:material:1.0.0'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsImplementation project(path: modulePrefix + 'extension-flac') withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
withExtensionsImplementation project(path: modulePrefix + 'extension-ima') withExtensionsImplementation project(path: modulePrefix + 'extension-ima')
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.demo"> package="com.google.android.exoplayer2.demo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
...@@ -33,11 +34,14 @@ ...@@ -33,11 +34,14 @@
android:banner="@drawable/ic_banner" android:banner="@drawable/ic_banner"
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:name="com.google.android.exoplayer2.demo.DemoApplication"> android:requestLegacyExternalStorage="true"
android:name="com.google.android.exoplayer2.demo.DemoApplication"
tools:ignore="UnusedAttribute">
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity" <activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:label="@string/application_name"> android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
......
...@@ -208,6 +208,13 @@ ...@@ -208,6 +208,13 @@
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"drm_session_for_clear_types": ["audio", "video"]
} }
] ]
}, },
...@@ -330,11 +337,11 @@ ...@@ -330,11 +337,11 @@
"samples": [ "samples": [
{ {
"name": "Super speed", "name": "Super speed",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
}, },
{ {
"name": "Super speed (PlayReady)", "name": "Super speed (PlayReady)",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
"drm_scheme": "playready" "drm_scheme": "playready"
} }
] ]
...@@ -376,44 +383,52 @@ ...@@ -376,44 +383,52 @@
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"name": "Apple AAC 10s", "name": "Apple 10s (AAC)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
}, },
{ {
"name": "Apple TS 10s", "name": "Apple 10s (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
}, },
{ {
"name": "Android screens (Matroska)", "name": "Android screens (MKV)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}, },
{ {
"name": "Screens 360P (WebM,VP9,No Audio)", "name": "Screens 360p video (WebM,VP9)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
}, },
{ {
"name": "Screens 480p (FMP4,H264,No Audio)", "name": "Screens 480p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
}, },
{ {
"name": "Screens 1080p (FMP4,H264, No Audio)", "name": "Screens 1080p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
}, },
{ {
"name": "Screens (FMP4,AAC Audio)", "name": "Screens audio (FMP4)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
}, },
{ {
"name": "Google Play (MP3 Audio)", "name": "Google Play (MP3)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
}, },
{ {
"name": "Google Play (Ogg/Vorbis Audio)", "name": "Google Play (Ogg/Vorbis)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
}, },
{ {
"name": "Big Buck Bunny (FLV Video)", "name": "Google Play (FLAC)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
},
{
"name": "Big Buck Bunny video (FLV)",
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
},
{
"name": "Big Buck Bunny 480p video (MP4,AV1)",
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
} }
] ]
}, },
...@@ -447,23 +462,27 @@ ...@@ -447,23 +462,27 @@
}, },
{ {
"name": "Clear -> Enc -> Clear -> Enc -> Enc", "name": "Clear -> Enc -> Clear -> Enc -> Enc",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"playlist": [ "playlist": [
{ {
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
} }
] ]
} }
...@@ -578,5 +597,24 @@ ...@@ -578,5 +597,24 @@
"spherical_stereo_mode": "top_bottom" "spherical_stereo_mode": "top_bottom"
} }
] ]
},
{
"name": "Subtitles",
"samples": [
{
"name": "TTML",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "en"
},
{
"name": "SSA/ASS position & alignment",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
"subtitle_mime_type": "text/x-ssa",
"subtitle_language": "en"
}
]
} }
] ]
...@@ -18,34 +18,41 @@ package com.google.android.exoplayer2.demo; ...@@ -18,34 +18,41 @@ package com.google.android.exoplayer2.demo;
import android.app.Application; import android.app.Application;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.FileDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException;
/** /**
* Placeholder application to facilitate overriding Application methods for debugging and testing. * Placeholder application to facilitate overriding Application methods for debugging and testing.
*/ */
public class DemoApplication extends Application { public class DemoApplication extends Application {
private static final String TAG = "DemoApplication";
private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
protected String userAgent; protected String userAgent;
private DatabaseProvider databaseProvider;
private File downloadDirectory; private File downloadDirectory;
private Cache downloadCache; private Cache downloadCache;
private DownloadManager downloadManager; private DownloadManager downloadManager;
...@@ -82,7 +89,8 @@ public class DemoApplication extends Application { ...@@ -82,7 +89,8 @@ public class DemoApplication extends Application {
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(this, extensionRendererMode); return new DefaultRenderersFactory(/* context= */ this)
.setExtensionRendererMode(extensionRendererMode);
} }
public DownloadManager getDownloadManager() { public DownloadManager getDownloadManager() {
...@@ -95,33 +103,51 @@ public class DemoApplication extends Application { ...@@ -95,33 +103,51 @@ public class DemoApplication extends Application {
return downloadTracker; return downloadTracker;
} }
protected synchronized Cache getDownloadCache() {
if (downloadCache == null) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
downloadCache =
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
}
return downloadCache;
}
private synchronized void initDownloadManager() { private synchronized void initDownloadManager() {
if (downloadManager == null) { if (downloadManager == null) {
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
upgradeActionFile(
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
upgradeActionFile(
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
DownloaderConstructorHelper downloaderConstructorHelper = DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
this, this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
new DefaultDownloaderFactory(downloaderConstructorHelper),
MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
DownloadManager.DEFAULT_REQUIREMENTS);
downloadTracker = downloadTracker =
new DownloadTracker( new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
/* context= */ this,
buildDataSourceFactory(),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
downloadManager.addListener(downloadTracker);
} }
} }
private synchronized Cache getDownloadCache() { private void upgradeActionFile(
if (downloadCache == null) { String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); try {
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor()); ActionFileUpgradeUtil.upgradeAndDelete(
new File(getDownloadDirectory(), fileName),
/* downloadIdProvider= */ null,
downloadIndex,
/* deleteOnFailure= */ true,
addNewDownloadsAsCompleted);
} catch (IOException e) {
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
} }
return downloadCache; }
private DatabaseProvider getDatabaseProvider() {
if (databaseProvider == null) {
databaseProvider = new ExoDatabaseProvider(this);
}
return databaseProvider;
} }
private File getDownloadDirectory() { private File getDownloadDirectory() {
...@@ -134,12 +160,12 @@ public class DemoApplication extends Application { ...@@ -134,12 +160,12 @@ public class DemoApplication extends Application {
return downloadDirectory; return downloadDirectory;
} }
private static CacheDataSourceFactory buildReadOnlyCacheDataSource( protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DefaultDataSourceFactory upstreamFactory, Cache cache) { DataSource.Factory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory( return new CacheDataSourceFactory(
cache, cache,
upstreamFactory, upstreamFactory,
new FileDataSourceFactory(), new FileDataSource.Factory(),
/* cacheWriteDataSinkFactory= */ null, /* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
/* eventListener= */ null); /* eventListener= */ null);
......
...@@ -16,13 +16,14 @@ ...@@ -16,13 +16,14 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Notification; import android.app.Notification;
import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.DownloadState;
import com.google.android.exoplayer2.scheduler.PlatformScheduler; import com.google.android.exoplayer2.scheduler.PlatformScheduler;
import com.google.android.exoplayer2.ui.DownloadNotificationUtil; import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.List;
/** A service for downloading media. */ /** A service for downloading media. */
public class DemoDownloadService extends DownloadService { public class DemoDownloadService extends DownloadService {
...@@ -33,16 +34,25 @@ public class DemoDownloadService extends DownloadService { ...@@ -33,16 +34,25 @@ public class DemoDownloadService extends DownloadService {
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
private DownloadNotificationHelper notificationHelper;
public DemoDownloadService() { public DemoDownloadService() {
super( super(
FOREGROUND_NOTIFICATION_ID, FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID, CHANNEL_ID,
R.string.exo_download_notification_channel_name); R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
} }
@Override @Override
public void onCreate() {
super.onCreate();
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
}
@Override
protected DownloadManager getDownloadManager() { protected DownloadManager getDownloadManager() {
return ((DemoApplication) getApplication()).getDownloadManager(); return ((DemoApplication) getApplication()).getDownloadManager();
} }
...@@ -53,35 +63,26 @@ public class DemoDownloadService extends DownloadService { ...@@ -53,35 +63,26 @@ public class DemoDownloadService extends DownloadService {
} }
@Override @Override
protected Notification getForegroundNotification(DownloadState[] downloadStates) { protected Notification getForegroundNotification(List<Download> downloads) {
return DownloadNotificationUtil.buildProgressNotification( return notificationHelper.buildProgressNotification(
/* context= */ this, R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
R.drawable.ic_download,
CHANNEL_ID,
/* contentIntent= */ null,
/* message= */ null,
downloadStates);
} }
@Override @Override
protected void onDownloadStateChanged(DownloadState downloadState) { protected void onDownloadChanged(Download download) {
Notification notification = null; Notification notification;
if (downloadState.state == DownloadState.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
notification = notification =
DownloadNotificationUtil.buildDownloadCompletedNotification( notificationHelper.buildDownloadCompletedNotification(
/* context= */ this,
R.drawable.ic_download_done, R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(downloadState.customMetadata)); Util.fromUtf8Bytes(download.request.data));
} else if (downloadState.state == DownloadState.STATE_FAILED) { } else if (download.state == Download.STATE_FAILED) {
notification = notification =
DownloadNotificationUtil.buildDownloadFailedNotification( notificationHelper.buildDownloadFailedNotification(
/* context= */ this,
R.drawable.ic_download_done, R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(downloadState.customMetadata)); Util.fromUtf8Bytes(download.request.data));
} else { } else {
return; return;
} }
......
...@@ -42,7 +42,15 @@ ...@@ -42,7 +42,15 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone"/> android:visibility="gone">
<Button android:id="@+id/select_tracks_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/track_selection_title"
android:enabled="false"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
......
...@@ -14,40 +14,46 @@ ...@@ -14,40 +14,46 @@
limitations under the License. limitations under the License.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:id="@+id/track_selection_dialog_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.tabs.TabLayout
android:id="@+id/track_selection_dialog_tab_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="12dp" app:tabGravity="fill"
android:paddingEnd="12dp" app:tabMode="fixed"/>
android:orientation="horizontal"
android:gravity="center_vertical"> </androidx.viewpager.widget.ViewPager>
<LinearLayout <LinearLayout
android:layout_width="0dp" android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:gravity="end">
android:orientation="vertical">
<TextView <Button
android:id="@+id/track_title" android:id="@+id/track_selection_dialog_cancel_button"
android:textStyle="bold"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="4dp"/> android:text="@android:string/cancel"
style="?android:attr/borderlessButtonStyle"/>
<TextView <Button
android:id="@+id/track_desc" android:id="@+id/track_selection_dialog_ok_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="4dp"/> android:text="@android:string/ok"
style="?android:attr/borderlessButtonStyle"/>
</LinearLayout> </LinearLayout>
<ImageButton
android:id="@+id/edit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:contentDescription="@string/download_edit_track"
android:src="@drawable/ic_edit"/>
</LinearLayout> </LinearLayout>
...@@ -13,13 +13,18 @@ ...@@ -13,13 +13,18 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/prefer_extension_decoders" <item android:id="@+id/prefer_extension_decoders"
android:title="@string/prefer_extension_decoders" android:title="@string/prefer_extension_decoders"
android:showAsAction="never" android:checkable="true"
android:checkable="true"/> app:showAsAction="never"/>
<item android:id="@+id/random_abr" <item android:id="@+id/random_abr"
android:title="@string/random_abr" android:title="@string/random_abr"
android:showAsAction="never" android:checkable="true"
android:checkable="true"/> app:showAsAction="never"/>
<item android:id="@+id/tunneling"
android:title="@string/tunneling"
android:checkable="true"
app:showAsAction="never"/>
</menu> </menu>
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
<string name="application_name">ExoPlayer</string> <string name="application_name">ExoPlayer</string>
<string name="track_selection_title">Select tracks</string>
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string> <string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string> <string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
...@@ -27,7 +29,7 @@ ...@@ -27,7 +29,7 @@
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string> <string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string> <string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string> <string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
...@@ -51,9 +53,7 @@ ...@@ -51,9 +53,7 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string> <string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="download_edit_track">Edit selection</string> <string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
<string name="download_preparing">Preparing download…</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
...@@ -63,10 +63,14 @@ ...@@ -63,10 +63,14 @@
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string> <string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
<string name="download_live_unsupported">This demo app does not support downloading live content</string>
<string name="download_ads_unsupported">IMA does not support offline ads</string> <string name="download_ads_unsupported">IMA does not support offline ads</string>
<string name="prefer_extension_decoders">Prefer extension decoders</string> <string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="random_abr">Enable random ABR</string> <string name="random_abr">Enable random ABR</string>
<string name="tunneling">Request multimedia tunneling</string>
</resources> </resources>
...@@ -15,13 +15,16 @@ ...@@ -15,13 +15,16 @@
--> -->
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PlayerTheme" parent="android:Theme.Holo"> <style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:windowNoTitle">true</item> <item name="windowNoTitle">false</item>
</style>
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
</style> </style>
<style name="PlayerTheme.Spherical"> <style name="PlayerTheme.Spherical">
<item name="surface_type">spherical_view</item> <item name="surface_type">spherical_gl_surface_view</item>
</style> </style>
</resources> </resources>
# ExoPlayer SurfaceControl demo
This app demonstrates how to use the [SurfaceControl][] API to redirect video
output from ExoPlayer between different views or off-screen. `SurfaceControl`
is new in Android 10, so the app requires `minSdkVersion` 29.
The app layout has a grid of `SurfaceViews`. Initially video is output to one
of the views. Tap a `SurfaceView` to move video output to it. You can also tap
the buttons at the top of the activity to move video output off-screen, to a
full-screen `SurfaceView` or to a new activity.
When using `SurfaceControl`, the `MediaCodec` always has the same surface
attached to it, which can be freely 'reparented' to any `SurfaceView` (or
off-screen) without any interruptions to playback. This works better than
calling `MediaCodec.setOutputSurface` to change the output surface of the codec
because `MediaCodec` does not re-render its last frame when that method is
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).
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl
// Copyright (C) 2017 The Android Open Source Project // Copyright (C) 2019 The Android Open Source Project
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
...@@ -16,7 +16,6 @@ apply plugin: 'com.android.application' ...@@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
...@@ -26,8 +25,8 @@ android { ...@@ -26,8 +25,8 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion 29
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
...@@ -36,13 +35,10 @@ android { ...@@ -36,13 +35,10 @@ android {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt') proguardFiles getDefaultProguardFile('proguard-android.txt')
} }
debug {
jniDebuggable = true
}
} }
lintOptions { lintOptions {
// The demo app does not have translations. // This demo app does not have translations.
disable 'MissingTranslation' disable 'MissingTranslation'
} }
} }
...@@ -51,10 +47,5 @@ dependencies { ...@@ -51,10 +47,5 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project <!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
...@@ -14,24 +14,28 @@ ...@@ -14,24 +14,28 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.imademo"> package="com.google.android.exoplayer2.surfacedemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk/> <uses-sdk/>
<uses-permission android:name="android.permission.INTERNET"/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
android:largeHeap="true" android:allowBackup="false"> <application
android:allowBackup="false"
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity" android:icon="@mipmap/ic_launcher"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:label="@string/application_name">
android:label="@string/application_name" <activity android:name=".MainActivity">
android:theme="@style/PlayerTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.surfacedemo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<GridLayout
android:id="@+id/grid_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:columnCount="3"/>
<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/player_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:show_timeout="0"/>
</LinearLayout>
<SurfaceView
android:id="@+id/full_screen_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project <!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
...@@ -15,10 +15,9 @@ ...@@ -15,10 +15,9 @@
--> -->
<resources> <resources>
<string name="application_name">Exo IMA Demo</string> <string name="application_name">ExoPlayer SurfaceControl demo</string>
<string name="no_output_label">No output</string>
<string name="content_url"><![CDATA[https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv]]></string> <string name="full_screen_label">Full screen</string>
<string name="new_activity_label">New activity</string>
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
</resources> </resources>
# ExoPlayer AV1 extension #
The AV1 extension provides `Libgav1VideoRenderer`, which uses libgav1 native
library to decode AV1 videos.
## License note ##
Please note that whilst the code in this repository is licensed under
[Apache 2.0][], using this extension also requires building and including one or
more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
In addition, it's necessary to fetch cpu_features library and libgav1 with its
dependencies as follows:
* Set the following environment variables:
```
cd "<path to exoplayer checkout>"
EXOPLAYER_ROOT="$(pwd)"
AV1_EXT_PATH="${EXOPLAYER_ROOT}/extensions/av1/src/main"
```
* Fetch cpu_features library:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://github.com/google/cpu_features
```
* Fetch libgav1:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
```
* Fetch Abseil:
```
cd "${AV1_EXT_PATH}/jni/libgav1" && \
git clone https://github.com/abseil/abseil-cpp.git third_party/abseil-cpp
```
* [Install CMake][].
Having followed these steps, gradle will build the extension automatically when
run on the command line or via Android Studio, using [CMake][] and [Ninja][]
to configure and build libgav1 and the extension's [JNI wrapper library][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Install CMake]: https://developer.android.com/studio/projects/install-ndk
[CMake]: https://cmake.org/
[Ninja]: https://ninja-build.org
[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc
## Build instructions (Windows) ##
We do not provide support for building this extension on Windows, however it
should be possible to follow the Linux instructions in [Windows PowerShell][].
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Using the extension ##
Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `Libgav1VideoRenderer`.
How you do this depends on which player API you're using:
* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
you can enable using the extension by setting the `extensionRendererMode`
parameter of the `DefaultRenderersFactory` constructor to
`EXTENSION_RENDERER_MODE_ON`. This will use `Libgav1VideoRenderer` for
playback if `MediaCodecVideoRenderer` doesn't support decoding the input AV1
stream. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `Libgav1VideoRenderer`
priority over `MediaCodecVideoRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `Libvgav1VideoRenderer`
to the output list in `buildVideoRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a
`Libgav1VideoRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
* If you're using `ExoPlayer.Builder`, pass a `Libgav1VideoRenderer` in the
array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation.
You need to make sure you are passing a `Libgav1VideoRenderer` to the player and
then you need to implement your own logic to use the renderer for a given track.
## Using the extension in the demo application ##
To try out playback using the extension in the [demo application][], see
[enabling extension decoders][].
[demo application]: https://exoplayer.dev/demo-application.html
[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
## Rendering options ##
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.
Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.av1.*`
belong to this module.
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
// Copyright (C) 2018 The Android Open Source Project // Copyright (C) 2019 The Android Open Source Project
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
...@@ -11,12 +11,11 @@ ...@@ -11,12 +11,11 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../constants.gradle' apply from: '../../constants.gradle'
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
...@@ -26,18 +25,49 @@ android { ...@@ -26,18 +25,49 @@ android {
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
externalNativeBuild {
cmake {
// Debug CMake build type causes video frames to drop,
// so native library should always use Release build type.
arguments "-DCMAKE_BUILD_TYPE=Release"
targets "gav1JNI"
}
}
}
// 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'
} }
lintOptions { sourceSets.main {
// Robolectric depends on BouncyCastle, which depends on javax.naming, // As native JNI library build is invoked from gradle, this is
// which is not part of Android. // not needed. However, it exposes the built library and keeps
disable 'InvalidPackage' // 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.
if (project.file('src/main/jni/libgav1').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
android.externalNativeBuild.cmake.version = '3.7.1+'
}
dependencies { dependencies {
api 'org.robolectric:robolectric:' + robolectricVersion
api project(modulePrefix + 'testutils')
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
}
ext {
javadocTitle = 'AV1 extension'
} }
apply from: '../../javadoc_library.gradle'
# Proguard rules specific to the AV1 extension.
# This prevents the names of native methods from being obfuscated.
-keepclasseswithmembernames class * {
native <methods>;
}
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project <!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.testutil"/> <manifest package="com.google.android.exoplayer2.ext.av1"/>
/*
* Copyright (C) 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.av1;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import java.nio.ByteBuffer;
/** Gav1 decoder. */
/* package */ final class Gav1Decoder
extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, Gav1DecoderException> {
// LINT.IfChange
private static final int GAV1_ERROR = 0;
private static final int GAV1_OK = 1;
private static final int GAV1_DECODE_ONLY = 2;
// LINT.ThenChange(../../../../../../../jni/gav1_jni.cc)
private final long gav1DecoderContext;
@C.VideoOutputMode private volatile int outputMode;
/**
* Creates a Gav1Decoder.
*
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
* @param initialInputBufferSize The initial size of each input buffer, in bytes.
* @param threads Number of threads libgav1 will use to decode.
* @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
*/
public Gav1Decoder(
int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, int threads)
throws Gav1DecoderException {
super(
new VideoDecoderInputBuffer[numInputBuffers],
new VideoDecoderOutputBuffer[numOutputBuffers]);
if (!Gav1Library.isAvailable()) {
throw new Gav1DecoderException("Failed to load decoder native library.");
}
gav1DecoderContext = gav1Init(threads);
if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Failed to initialize decoder. Error: " + gav1GetErrorMessage(gav1DecoderContext));
}
setInitialInputBufferSize(initialInputBufferSize);
}
@Override
public String getName() {
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();
}
@Override
protected VideoDecoderOutputBuffer createOutputBuffer() {
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
}
@Nullable
@Override
protected Gav1DecoderException decode(
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
int inputSize = inputData.limit();
if (gav1Decode(gav1DecoderContext, inputData, inputSize) == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1Decode error: " + gav1GetErrorMessage(gav1DecoderContext));
}
boolean decodeOnly = inputBuffer.isDecodeOnly();
if (!decodeOnly) {
outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
}
// We need to dequeue the decoded frame from the decoder even when the input data is
// decode-only.
int getFrameResult = gav1GetFrame(gav1DecoderContext, outputBuffer, decodeOnly);
if (getFrameResult == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1GetFrame error: " + gav1GetErrorMessage(gav1DecoderContext));
}
if (getFrameResult == GAV1_DECODE_ONLY) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
if (!decodeOnly) {
outputBuffer.colorInfo = inputBuffer.colorInfo;
}
return null;
}
@Override
protected Gav1DecoderException createUnexpectedDecodeException(Throwable error) {
return new Gav1DecoderException("Unexpected decode error", error);
}
@Override
public void release() {
super.release();
gav1Close(gav1DecoderContext);
}
@Override
protected void releaseOutputBuffer(VideoDecoderOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to gav1ReleaseFrame.
if (buffer.mode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
gav1ReleaseFrame(gav1DecoderContext, buffer);
}
super.releaseOutputBuffer(buffer);
}
/**
* Renders output buffer to the given surface. Must only be called when in {@link
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
*
* @param outputBuffer Output buffer.
* @param surface Output surface.
* @throws Gav1DecoderException Thrown if called with invalid output mode or frame rendering
* fails.
*/
public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (outputBuffer.mode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) {
throw new Gav1DecoderException("Invalid output mode.");
}
if (gav1RenderFrame(gav1DecoderContext, surface, outputBuffer) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Buffer render error: " + gav1GetErrorMessage(gav1DecoderContext));
}
}
/**
* Initializes a libgav1 decoder.
*
* @param threads Number of threads to be used by a libgav1 decoder.
* @return The address of the decoder context or {@link #GAV1_ERROR} if there was an error.
*/
private native long gav1Init(int threads);
/**
* Deallocates the decoder context.
*
* @param context Decoder context.
*/
private native void gav1Close(long context);
/**
* Decodes the encoded data passed.
*
* @param context Decoder context.
* @param encodedData Encoded data.
* @param length Length of the data buffer.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1Decode(long context, ByteBuffer encodedData, int length);
/**
* Gets the decoded frame.
*
* @param context Decoder context.
* @param outputBuffer Output buffer for the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_DECODE_ONLY} if successful but the frame
* is decode-only, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1GetFrame(
long context, VideoDecoderOutputBuffer outputBuffer, boolean decodeOnly);
/**
* Renders the frame to the surface. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param surface Output surface.
* @param outputBuffer Output buffer with the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occured.
*/
private native int gav1RenderFrame(
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
/**
* Releases the frame. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param outputBuffer Output buffer.
*/
private native void gav1ReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer);
/**
* Returns a human-readable string describing the last error encountered in the given context.
*
* @param context Decoder context.
* @return A string describing the last encountered error.
*/
private native String gav1GetErrorMessage(long context);
/**
* Returns whether an error occured.
*
* @param context Decoder context.
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured.
*/
private native int gav1CheckError(long context);
}
/*
* Copyright (C) 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.av1;
import com.google.android.exoplayer2.video.VideoDecoderException;
/** Thrown when a libgav1 decoder error occurs. */
public final class Gav1DecoderException extends VideoDecoderException {
/* package */ Gav1DecoderException(String message) {
super(message);
}
/* package */ Gav1DecoderException(String message, Throwable cause) {
super(message, cause);
}
}
/*
* Copyright (C) 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.av1;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
/** Configures and queries the underlying native library. */
public final class Gav1Library {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.gav1");
}
private static final LibraryLoader LOADER = new LibraryLoader("gav1JNI");
private Gav1Library() {}
/** Returns whether the underlying library is available, loading it if necessary. */
public static boolean isAvailable() {
return LOADER.isAvailable();
}
}
/*
* Copyright (C) 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.av1;
import static java.lang.Runtime.getRuntime;
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
import com.google.android.exoplayer2.video.VideoDecoderException;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
/**
* Decodes and renders video using libgav1 decoder.
*
* <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
* on the playback thread:
*
* <ul>
* <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
* should be the target {@link Surface}, or null.
* <li>Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output
* buffer renderer. The message payload should be the target {@link
* VideoDecoderOutputBufferRenderer}, or null.
* </ul>
*/
public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
/* Default size based on 720p resolution video compressed by a factor of two. */
private static final int DEFAULT_INPUT_BUFFER_SIZE =
Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2;
/** The number of input buffers. */
private final int numInputBuffers;
/**
* The number of output buffers. The renderer may limit the minimum possible value due to
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
private final int numOutputBuffers;
private final int threads;
@Nullable private Gav1Decoder decoder;
/**
* Creates a Libgav1VideoRenderer.
*
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
public Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* threads= */ getRuntime().availableProcessors(),
DEFAULT_NUM_OF_INPUT_BUFFERS,
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
}
/**
* Creates a Libgav1VideoRenderer.
*
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
* @param threads Number of threads libgav1 will use to decode.
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
*/
public Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
int threads,
int numInputBuffers,
int numOutputBuffers) {
super(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false);
this.threads = threads;
this.numInputBuffers = numInputBuffers;
this.numOutputBuffers = numOutputBuffers;
}
@Override
@Capabilities
protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|| !Gav1Library.isAvailable()) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
}
if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
}
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
}
@Override
protected SimpleDecoder<
VideoDecoderInputBuffer,
? extends VideoDecoderOutputBuffer,
? extends VideoDecoderException>
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws VideoDecoderException {
TraceUtil.beginSection("createGav1Decoder");
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
Gav1Decoder decoder =
new Gav1Decoder(numInputBuffers, numOutputBuffers, initialInputBufferSize, threads);
this.decoder = decoder;
TraceUtil.endSection();
return decoder;
}
@Override
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (decoder == null) {
throw new Gav1DecoderException(
"Failed to render output buffer to surface: decoder is not initialized.");
}
decoder.renderToSurface(outputBuffer, surface);
outputBuffer.release();
}
@Override
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
}
// PlayerMessage.Target implementation.
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == C.MSG_SET_SURFACE) {
setOutputSurface((Surface) message);
} else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
} else {
super.handleMessage(messageType, message);
}
}
}
/*
* Copyright (C) 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.av1;
import com.google.android.exoplayer2.util.NonNullApi;
# 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)
# Devices using armeabi-v7a are not required to support
# Neon which is why Neon is disabled by default for
# armeabi-v7a build. This flag enables it.
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
add_compile_options("-mfpu=neon")
add_compile_options("-fPIC")
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}"
EXCLUDE_FROM_ALL)
# Build libgav1.
add_subdirectory("${libgav1_root}"
"${libgav1_build}"
EXCLUDE_FROM_ALL)
# Build libgav1JNI.
add_library(gav1JNI
SHARED
gav1_jni.cc)
# Locate NDK log library.
find_library(android_log_lib log)
# Link libgav1JNI against used libraries.
target_link_libraries(gav1JNI
PRIVATE android
PRIVATE cpu_features
PRIVATE libgav1_static
PRIVATE ${android_log_lib})
# Specify output directory for libgav1JNI.
set_target_properties(gav1JNI PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
${libgav1_jni_output_directory})
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
The cast extension is a [Player][] implementation that controls playback on a The cast extension is a [Player][] implementation that controls playback on a
Cast receiver app. Cast receiver app.
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html [Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html
## Getting the extension ## ## Getting the extension ##
......
...@@ -16,7 +16,6 @@ apply plugin: 'com.android.library' ...@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
...@@ -24,32 +23,22 @@ android { ...@@ -24,32 +23,22 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.1.2' api 'com.google.android.gms:play-services-cast-framework:17.0.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example via:
// com.google.android.gms:play-services-cast-framework:15.0.1
// |-- com.android.support:mediarouter-v7:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
} }
ext { ext {
......
# Proguard rules specific to the Cast extension.
# DefaultCastOptionsProvider is commonly referred to only by the app's manifest.
-keep class com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider
...@@ -15,53 +15,101 @@ ...@@ -15,53 +15,101 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.Nullable; import android.util.SparseArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** /**
* A {@link Timeline} for Cast media queues. * A {@link Timeline} for Cast media queues.
*/ */
/* package */ final class CastTimeline extends Timeline { /* package */ final class CastTimeline extends Timeline {
/** Holds {@link Timeline} related data for a Cast media item. */
public static final class ItemData {
/** Holds no media information. */
public static final ItemData EMPTY = new ItemData();
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
public final long durationUs;
/**
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public final long defaultPositionUs;
/** Whether the item is live content, or {@code false} if unknown. */
public final boolean isLive;
private ItemData() {
this(
/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */
C.TIME_UNSET,
/* isLive= */ false);
}
/**
* Creates an instance.
*
* @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}.
* @param isLive See {@link #isLive}.
*/
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.isLive = isLive;
}
/**
* Returns a copy of this instance with the given values.
*
* @param durationUs The duration in microseconds, or {@link C#TIME_UNSET} if unknown.
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
* if unknown.
* @param isLive Whether the item is live, or {@code false} if unknown.
*/
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
if (durationUs == this.durationUs
&& defaultPositionUs == this.defaultPositionUs
&& isLive == this.isLive) {
return this;
}
return new ItemData(durationUs, defaultPositionUs, isLive);
}
}
/** {@link Timeline} for a cast queue that has no items. */
public static final CastTimeline EMPTY_CAST_TIMELINE = public static final CastTimeline EMPTY_CAST_TIMELINE =
new CastTimeline(Collections.emptyList(), Collections.emptyMap()); new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex; private final SparseIntArray idsToIndex;
private final int[] ids; private final int[] ids;
private final long[] durationsUs; private final long[] durationsUs;
private final long[] defaultPositionsUs; private final long[] defaultPositionsUs;
private final boolean[] isLive;
/** /**
* @param items A list of cast media queue items to represent. * Creates a Cast timeline from the given data.
* @param contentIdToDurationUsMap A map of content id to duration in microseconds. *
* @param itemIds The ids of the items in the timeline.
* @param itemIdToData Maps item ids to {@link ItemData}.
*/ */
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) { public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
int itemCount = items.size(); int itemCount = itemIds.length;
int index = 0;
idsToIndex = new SparseIntArray(itemCount); idsToIndex = new SparseIntArray(itemCount);
ids = new int[itemCount]; ids = Arrays.copyOf(itemIds, itemCount);
durationsUs = new long[itemCount]; durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount]; defaultPositionsUs = new long[itemCount];
for (MediaQueueItem item : items) { isLive = new boolean[itemCount];
int itemId = item.getItemId(); for (int i = 0; i < ids.length; i++) {
ids[index] = itemId; int id = ids[i];
idsToIndex.put(itemId, index); idsToIndex.put(id, i);
MediaInfo mediaInfo = item.getMedia(); ItemData data = itemIdToData.get(id, ItemData.EMPTY);
String contentId = mediaInfo.getContentId(); durationsUs[i] = data.durationUs;
durationsUs[index] = defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
contentIdToDurationUsMap.containsKey(contentId) isLive[i] = data.isLive;
? contentIdToDurationUsMap.get(contentId)
: CastUtils.getStreamDurationUs(mediaInfo);
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
index++;
} }
} }
...@@ -73,17 +121,19 @@ import java.util.Map; ...@@ -73,17 +121,19 @@ import java.util.Map;
} }
@Override @Override
public Window getWindow( public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex]; long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET; boolean isDynamic = durationUs == C.TIME_UNSET;
Object tag = setTag ? ids[windowIndex] : null;
return window.set( return window.set(
tag, /* uid= */ ids[windowIndex],
/* tag= */ ids[windowIndex],
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic, /* isSeekable= */ !isDynamic,
isDynamic, isDynamic,
isLive[windowIndex],
defaultPositionsUs[windowIndex], defaultPositionsUs[windowIndex],
durationUs, durationUs,
/* firstPeriodIndex= */ windowIndex, /* firstPeriodIndex= */ windowIndex,
...@@ -124,7 +174,8 @@ import java.util.Map; ...@@ -124,7 +174,8 @@ import java.util.Map;
CastTimeline that = (CastTimeline) other; CastTimeline that = (CastTimeline) other;
return Arrays.equals(ids, that.ids) return Arrays.equals(ids, that.ids)
&& Arrays.equals(durationsUs, that.durationsUs) && Arrays.equals(durationsUs, that.durationsUs)
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs); && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
&& Arrays.equals(isLive, that.isLive);
} }
@Override @Override
...@@ -132,6 +183,7 @@ import java.util.Map; ...@@ -132,6 +183,7 @@ import java.util.Map;
int result = Arrays.hashCode(ids); int result = Arrays.hashCode(ids);
result = 31 * result + Arrays.hashCode(durationsUs); result = 31 * result + Arrays.hashCode(durationsUs);
result = 31 * result + Arrays.hashCode(defaultPositionsUs); result = 31 * result + Arrays.hashCode(defaultPositionsUs);
result = 31 * result + Arrays.hashCode(isLive);
return result; return result;
} }
......
...@@ -15,53 +15,94 @@ ...@@ -15,53 +15,94 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
import java.util.HashMap; import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
/** /**
* Creates {@link CastTimeline}s from cast receiver app media status. * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
* *
* <p>This class keeps track of the duration reported by the current item to fill any missing * <p>This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553]. * durations in the media queue items [See internal: b/65152553].
*/ */
/* package */ final class CastTimelineTracker { /* package */ final class CastTimelineTracker {
private final HashMap<String, Long> contentIdToDurationUsMap; private final SparseArray<CastTimeline.ItemData> itemIdToData;
private final HashSet<String> scratchContentIdSet;
public CastTimelineTracker() { public CastTimelineTracker() {
contentIdToDurationUsMap = new HashMap<>(); itemIdToData = new SparseArray<>();
scratchContentIdSet = new HashSet<>();
} }
/** /**
* Returns a {@link CastTimeline} that represent the given {@code status}. * Returns a {@link CastTimeline} that represents the state of the given {@code
* remoteMediaClient}.
* *
* @param status The Cast media status. * <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
* @return A {@link CastTimeline} that represent the given {@code status}. * invocations of this method.
*
* @param remoteMediaClient The Cast media client.
* @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
*/ */
public CastTimeline getCastTimeline(MediaStatus status) { public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
MediaInfo mediaInfo = status.getMediaInfo(); int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
List<MediaQueueItem> items = status.getQueueItems(); if (itemIds.length > 0) {
removeUnusedDurationEntries(items); // Only remove unused items when there is something in the queue to avoid removing all entries
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
removeUnusedItemDataEntries(itemIds);
}
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
if (mediaStatus == null) {
return CastTimeline.EMPTY_CAST_TIMELINE;
}
int currentItemId = mediaStatus.getCurrentItemId();
updateItemData(
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
}
if (mediaInfo != null) { return new CastTimeline(itemIds, itemIdToData);
String contentId = mediaInfo.getContentId(); }
private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
long durationUs = CastUtils.getStreamDurationUs(mediaInfo); long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
contentIdToDurationUsMap.put(contentId, durationUs); if (durationUs == C.TIME_UNSET) {
durationUs = previousData.durationUs;
}
boolean isLive =
mediaInfo == null
? previousData.isLive
: mediaInfo.getStreamType() == MediaInfo.STREAM_TYPE_LIVE;
if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs;
} }
return new CastTimeline(items, contentIdToDurationUsMap); itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
} }
private void removeUnusedDurationEntries(List<MediaQueueItem> items) { private void removeUnusedItemDataEntries(int[] itemIds) {
scratchContentIdSet.clear(); HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
for (MediaQueueItem item : items) { for (int id : itemIds) {
scratchContentIdSet.add(item.getMedia().getContentId()); scratchItemIds.add(id);
}
int index = 0;
while (index < itemIdToData.size()) {
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
itemIdToData.removeAt(index);
} else {
index++;
}
} }
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
} }
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.CastStatusCodes;
...@@ -31,11 +32,13 @@ import com.google.android.gms.cast.MediaTrack; ...@@ -31,11 +32,13 @@ import com.google.android.gms.cast.MediaTrack;
* unknown or not applicable. * unknown or not applicable.
* *
* @param mediaInfo The media info to get the duration from. * @param mediaInfo The media info to get the duration from.
* @return The duration in microseconds. * @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
*/ */
public static long getStreamDurationUs(MediaInfo mediaInfo) { public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
long durationMs = if (mediaInfo == null) {
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION; return C.TIME_UNSET;
}
long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET; return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
} }
...@@ -109,6 +112,7 @@ import com.google.android.gms.cast.MediaTrack; ...@@ -109,6 +112,7 @@ import com.google.android.gms.cast.MediaTrack;
/* codecs= */ null, /* codecs= */ null,
/* bitrate= */ Format.NO_VALUE, /* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0, /* selectionFlags= */ 0,
/* roleFlags= */ 0,
mediaTrack.getLanguage()); mediaTrack.getLanguage());
} }
......
...@@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent; ...@@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions; import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider; import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider; import com.google.android.gms.cast.framework.SessionProvider;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
...@@ -27,16 +28,38 @@ import java.util.List; ...@@ -27,16 +28,38 @@ import java.util.List;
*/ */
public final class DefaultCastOptionsProvider implements OptionsProvider { public final class DefaultCastOptionsProvider implements OptionsProvider {
/**
* App id of the Default Media Receiver app. Apps that do not require DRM support may use this
* receiver receiver app ID.
*
* <p>See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver.
*/
public static final String APP_ID_DEFAULT_RECEIVER =
CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
/**
* App id for receiver app with rudimentary support for DRM.
*
* <p>This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for
* production use. In order to use DRM, custom receiver apps should be used. For environments that
* do not require DRM, the default receiver app should be used (see {@link
* #APP_ID_DEFAULT_RECEIVER}).
*/
// TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
// b/128603245].
public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
@Override @Override
public CastOptions getCastOptions(Context context) { public CastOptions getCastOptions(Context context) {
return new CastOptions.Builder() return new CastOptions.Builder()
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) .setReceiverApplicationId(APP_ID_DEFAULT_RECEIVER_WITH_DRM)
.setStopReceiverApplicationWhenEndingSession(true).build(); .setStopReceiverApplicationWhenEndingSession(true)
.build();
} }
@Override @Override
public List<SessionProvider> getAdditionalSessionProviders(Context context) { public List<SessionProvider> getAdditionalSessionProviders(Context context) {
return null; return Collections.emptyList();
} }
} }
/*
* Copyright (C) 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.cast;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.HashMap;
import java.util.Iterator;
import java.util.UUID;
import org.json.JSONException;
import org.json.JSONObject;
/** Default {@link MediaItemConverter} implementation. */
public final class DefaultMediaItemConverter implements MediaItemConverter {
private static final String KEY_MEDIA_ITEM = "mediaItem";
private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
private static final String KEY_URI = "uri";
private static final String KEY_TITLE = "title";
private static final String KEY_MIME_TYPE = "mimeType";
private static final String KEY_DRM_CONFIGURATION = "drmConfiguration";
private static final String KEY_UUID = "uuid";
private static final String KEY_LICENSE_URI = "licenseUri";
private static final String KEY_REQUEST_HEADERS = "requestHeaders";
@Override
public MediaItem toMediaItem(MediaQueueItem item) {
return getMediaItem(item.getMedia().getCustomData());
}
@Override
public MediaQueueItem toMediaQueueItem(MediaItem item) {
if (item.mimeType == null) {
throw new IllegalArgumentException("The item must specify its mimeType");
}
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
if (item.title != null) {
metadata.putString(MediaMetadata.KEY_TITLE, item.title);
}
MediaInfo mediaInfo =
new MediaInfo.Builder(item.uri.toString())
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(item.mimeType)
.setMetadata(metadata)
.setCustomData(getCustomData(item))
.build();
return new MediaQueueItem.Builder(mediaInfo).build();
}
// Deserialization.
private static MediaItem getMediaItem(JSONObject customData) {
try {
JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
MediaItem.Builder builder = new MediaItem.Builder();
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
if (mediaItemJson.has(KEY_TITLE)) {
builder.setTitle(mediaItemJson.getString(KEY_TITLE));
}
if (mediaItemJson.has(KEY_MIME_TYPE)) {
builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
}
if (mediaItemJson.has(KEY_DRM_CONFIGURATION)) {
builder.setDrmConfiguration(
getDrmConfiguration(mediaItemJson.getJSONObject(KEY_DRM_CONFIGURATION)));
}
return builder.build();
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
private static DrmConfiguration getDrmConfiguration(JSONObject json) throws JSONException {
UUID uuid = UUID.fromString(json.getString(KEY_UUID));
Uri licenseUri = Uri.parse(json.getString(KEY_LICENSE_URI));
JSONObject requestHeadersJson = json.getJSONObject(KEY_REQUEST_HEADERS);
HashMap<String, String> requestHeaders = new HashMap<>();
for (Iterator<String> iterator = requestHeadersJson.keys(); iterator.hasNext(); ) {
String key = iterator.next();
requestHeaders.put(key, requestHeadersJson.getString(key));
}
return new DrmConfiguration(uuid, licenseUri, requestHeaders);
}
// Serialization.
private static JSONObject getCustomData(MediaItem item) {
JSONObject json = new JSONObject();
try {
json.put(KEY_MEDIA_ITEM, getMediaItemJson(item));
JSONObject playerConfigJson = getPlayerConfigJson(item);
if (playerConfigJson != null) {
json.put(KEY_PLAYER_CONFIG, playerConfigJson);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
return json;
}
private static JSONObject getMediaItemJson(MediaItem item) throws JSONException {
JSONObject json = new JSONObject();
json.put(KEY_URI, item.uri.toString());
json.put(KEY_TITLE, item.title);
json.put(KEY_MIME_TYPE, item.mimeType);
if (item.drmConfiguration != null) {
json.put(KEY_DRM_CONFIGURATION, getDrmConfigurationJson(item.drmConfiguration));
}
return json;
}
private static JSONObject getDrmConfigurationJson(DrmConfiguration drmConfiguration)
throws JSONException {
JSONObject json = new JSONObject();
json.put(KEY_UUID, drmConfiguration.uuid);
json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri);
json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.requestHeaders));
return json;
}
@Nullable
private static JSONObject getPlayerConfigJson(MediaItem item) throws JSONException {
DrmConfiguration drmConfiguration = item.drmConfiguration;
if (drmConfiguration == null) {
return null;
}
String drmScheme;
if (C.WIDEVINE_UUID.equals(drmConfiguration.uuid)) {
drmScheme = "widevine";
} else if (C.PLAYREADY_UUID.equals(drmConfiguration.uuid)) {
drmScheme = "playready";
} else {
return null;
}
JSONObject exoPlayerConfigJson = new JSONObject();
exoPlayerConfigJson.put("withCredentials", false);
exoPlayerConfigJson.put("protectionSystem", drmScheme);
if (drmConfiguration.licenseUri != null) {
exoPlayerConfigJson.put("licenseUrl", drmConfiguration.licenseUri);
}
if (!drmConfiguration.requestHeaders.isEmpty()) {
exoPlayerConfigJson.put("headers", new JSONObject(drmConfiguration.requestHeaders));
}
return exoPlayerConfigJson;
}
}
/*
* Copyright (C) 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.cast;
import com.google.android.gms.cast.MediaQueueItem;
/** Converts between {@link MediaItem} and the Cast SDK's {@link MediaQueueItem}. */
public interface MediaItemConverter {
/**
* Converts a {@link MediaItem} to a {@link MediaQueueItem}.
*
* @param mediaItem The {@link MediaItem}.
* @return An equivalent {@link MediaQueueItem}.
*/
MediaQueueItem toMediaQueueItem(MediaItem mediaItem);
/**
* Converts a {@link MediaQueueItem} to a {@link MediaItem}.
*
* @param mediaQueueItem The {@link MediaQueueItem}.
* @return The equivalent {@link MediaItem}.
*/
MediaItem toMediaItem(MediaQueueItem mediaQueueItem);
}
/*
* Copyright (C) 2018 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.cast;
/** Represents a sequence of {@link MediaItem MediaItems}. */
public interface MediaItemQueue {
/**
* Returns the item at the given index.
*
* @param index The index of the item to retrieve.
* @return The item at the given index.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
MediaItem get(int index);
/** Returns the number of items in this queue. */
int getSize();
/**
* Appends the given sequence of items to the queue.
*
* @param items The sequence of items to append.
*/
void add(MediaItem... items);
/**
* Adds the given sequence of items to the queue at the given position, so that the first of
* {@code items} is placed at the given index.
*
* @param index The index at which {@code items} will be inserted.
* @param items The sequence of items to append.
* @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.
*/
void add(int index, MediaItem... items);
/**
* Moves an existing item within the playlist.
*
* <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and
* immediately inserting it at position {@code indexTo}. If the moved item is being played at the
* moment of the invocation, playback will stick with the moved item.
*
* @param indexFrom The index of the item to move.
* @param indexTo The index at which the item will be placed after this operation.
* @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.
*/
void move(int indexFrom, int indexTo);
/**
* Removes an item from the queue.
*
* @param index The index of the item to remove from the queue.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
void remove(int index);
/**
* Removes a range of items from the queue.
*
* <p>Does nothing if an empty range ({@code from == exclusiveTo}) is passed.
*
* @param from The inclusive index at which the range to remove starts.
* @param exclusiveTo The exclusive index at which the range to remove ends.
* @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >
* exclusiveTo}.
*/
void removeRange(int from, int exclusiveTo);
/** Removes all items in the queue. */
void clear();
}
/*
* Copyright (C) 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.cast;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.ext.cast.test"/> <manifest package="com.google.android.exoplayer2.ext.cast.test">
<uses-sdk/>
</manifest>
/*
* Copyright (C) 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.cast;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Player;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManager;
import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
/** Tests for {@link CastPlayer}. */
@RunWith(AndroidJUnit4.class)
public class CastPlayerTest {
private CastPlayer castPlayer;
private RemoteMediaClient.Listener remoteMediaClientListener;
@Mock private RemoteMediaClient mockRemoteMediaClient;
@Mock private MediaStatus mockMediaStatus;
@Mock private MediaQueue mockMediaQueue;
@Mock private CastContext mockCastContext;
@Mock private SessionManager mockSessionManager;
@Mock private CastSession mockCastSession;
@Mock private Player.EventListener mockListener;
@Mock private PendingResult<RemoteMediaClient.MediaChannelResult> mockPendingResult;
@Captor
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
setResultCallbackArgumentCaptor;
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
@Before
public void setUp() {
initMocks(this);
when(mockCastContext.getSessionManager()).thenReturn(mockSessionManager);
when(mockSessionManager.getCurrentCastSession()).thenReturn(mockCastSession);
when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient);
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
// Make the remote media client present the same default values as ExoPlayer:
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
castPlayer = new CastPlayer(mockCastContext);
castPlayer.addListener(mockListener);
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
remoteMediaClientListener = listenerArgumentCaptor.getValue();
}
@Test
public void testSetPlayWhenReady_masksRemoteState() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.play();
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
// There is a status update in the middle, which should be hidden by masking.
remoteMediaClientListener.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the remoteMediaClient has updated its state according to the play() call.
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@Test
public void testSetPlayWhenReadyMasking_updatesUponResultChange() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.play();
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
// Upon result, the remote media client is still paused. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
}
@Test
public void testPlayWhenReady_changesOnStatusUpdates() {
assertThat(castPlayer.getPlayWhenReady()).isFalse();
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
remoteMediaClientListener.onStatusUpdated();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
assertThat(castPlayer.getPlayWhenReady()).isTrue();
}
@Test
public void testSetRepeatMode_masksRemoteState() {
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
// 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();
verifyNoMoreInteractions(mockListener);
// Upon result, the mediaStatus now exposes the new repeat mode.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@Test
public void testSetRepeatMode_updatesUponResultChange() {
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
// 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();
verifyNoMoreInteractions(mockListener);
// Upon result, the repeat mode is ALL. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL);
}
@Test
public void testRepeatMode_changesOnStatusUpdates() {
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
remoteMediaClientListener.onStatusUpdated();
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
}
}
...@@ -15,23 +15,23 @@ ...@@ -15,23 +15,23 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.testutil.TimelineAsserts;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
import java.util.ArrayList; import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.Collections;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link CastTimelineTracker}. */ /** Tests for {@link CastTimelineTracker}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public class CastTimelineTrackerTest { public class CastTimelineTrackerTest {
private static final long DURATION_1_MS = 1000;
private static final long DURATION_2_MS = 2000; private static final long DURATION_2_MS = 2000;
private static final long DURATION_3_MS = 3000; private static final long DURATION_3_MS = 3000;
private static final long DURATION_4_MS = 4000; private static final long DURATION_4_MS = 4000;
...@@ -39,91 +39,89 @@ public class CastTimelineTrackerTest { ...@@ -39,91 +39,89 @@ public class CastTimelineTrackerTest {
/** Tests that duration of the current media info is correctly propagated to the timeline. */ /** Tests that duration of the current media info is correctly propagated to the timeline. */
@Test @Test
public void testGetCastTimeline() { public void testGetCastTimelinePersistsDuration() {
MediaInfo mediaInfo;
MediaStatus status =
mockMediaStatus(
new int[] {1, 2, 3},
new String[] {"contentId1", "contentId2", "contentId3"},
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
CastTimelineTracker tracker = new CastTimelineTracker(); CastTimelineTracker tracker = new CastTimelineTracker();
mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS); RemoteMediaClient remoteMediaClient =
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 2,
/* currentDurationMs= */ DURATION_2_MS);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), tracker.getCastTimeline(remoteMediaClient),
C.msToUs(DURATION_1_MS),
C.TIME_UNSET, C.TIME_UNSET,
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_2_MS),
C.TIME_UNSET,
C.TIME_UNSET,
C.TIME_UNSET);
mediaInfo = getMediaInfo("contentId2", DURATION_2_MS); remoteMediaClient =
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3},
/* currentItemId= */ 3,
/* currentDurationMs= */ DURATION_3_MS);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(status), tracker.getCastTimeline(remoteMediaClient),
C.msToUs(DURATION_1_MS), C.TIME_UNSET,
C.msToUs(DURATION_2_MS), C.msToUs(DURATION_2_MS),
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_3_MS));
MediaStatus newStatus = remoteMediaClient =
mockMediaStatus( mockRemoteMediaClient(
new int[] {4, 1, 5, 3}, /* itemIds= */ new int[] {1, 3},
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"}, /* currentItemId= */ 3,
new long[] { /* currentDurationMs= */ DURATION_3_MS);
MediaInfo.UNKNOWN_DURATION,
MediaInfo.UNKNOWN_DURATION,
DURATION_5_MS,
MediaInfo.UNKNOWN_DURATION
});
mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));
C.TIME_UNSET,
C.msToUs(DURATION_1_MS),
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS); remoteMediaClient =
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 4,
/* currentDurationMs= */ DURATION_4_MS);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET, C.TIME_UNSET,
C.msToUs(DURATION_1_MS), C.TIME_UNSET,
C.msToUs(DURATION_5_MS), C.msToUs(DURATION_3_MS),
C.msToUs(DURATION_3_MS)); C.msToUs(DURATION_4_MS),
C.TIME_UNSET);
mediaInfo = getMediaInfo("contentId4", DURATION_4_MS); remoteMediaClient =
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); mockRemoteMediaClient(
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
/* currentItemId= */ 5,
/* currentDurationMs= */ DURATION_5_MS);
TimelineAsserts.assertPeriodDurations( TimelineAsserts.assertPeriodDurations(
tracker.getCastTimeline(newStatus), tracker.getCastTimeline(remoteMediaClient),
C.TIME_UNSET,
C.TIME_UNSET,
C.msToUs(DURATION_3_MS),
C.msToUs(DURATION_4_MS), C.msToUs(DURATION_4_MS),
C.msToUs(DURATION_1_MS), C.msToUs(DURATION_5_MS));
C.msToUs(DURATION_5_MS),
C.msToUs(DURATION_3_MS));
} }
private static MediaStatus mockMediaStatus( private static RemoteMediaClient mockRemoteMediaClient(
int[] itemIds, String[] contentIds, long[] durationsMs) { int[] itemIds, int currentItemId, long currentDurationMs) {
ArrayList<MediaQueueItem> items = new ArrayList<>(); RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
for (int i = 0; i < contentIds.length; i++) {
MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
items.add(item);
}
MediaStatus status = Mockito.mock(MediaStatus.class); MediaStatus status = Mockito.mock(MediaStatus.class);
Mockito.when(status.getQueueItems()).thenReturn(items); Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
return status; Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
MediaQueue mediaQueue = mockMediaQueue(itemIds);
Mockito.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);
return mediaQueue;
} }
private static MediaInfo getMediaInfo(String contentId, long durationMs) { private static MediaInfo getMediaInfo(long durationMs) {
return new MediaInfo.Builder(contentId) return new MediaInfo.Builder(/*contentId= */ "")
.setStreamDuration(durationMs) .setStreamDuration(durationMs)
.setContentType(MimeTypes.APPLICATION_MP4) .setContentType(MimeTypes.APPLICATION_MP4)
.setStreamType(MediaInfo.STREAM_TYPE_NONE) .setStreamType(MediaInfo.STREAM_TYPE_NONE)
......
/*
* Copyright (C) 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.cast;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Test for {@link DefaultMediaItemConverter}. */
@RunWith(AndroidJUnit4.class)
public class DefaultMediaItemConverterTest {
@Test
public void serialize_deserialize_minimal() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item = builder.setUri(Uri.parse("http://example.com")).setMimeType("mime").build();
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
MediaItem reconstructedItem = converter.toMediaItem(queueItem);
assertThat(reconstructedItem).isEqualTo(item);
}
@Test
public void serialize_deserialize_complete() {
MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item =
builder
.setUri(Uri.parse("http://example.com"))
.setTitle("title")
.setMimeType("mime")
.setDrmConfiguration(
new DrmConfiguration(
C.WIDEVINE_UUID,
Uri.parse("http://license.com"),
Collections.singletonMap("key", "value")))
.build();
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
MediaQueueItem queueItem = converter.toMediaQueueItem(item);
MediaItem reconstructedItem = converter.toMediaItem(queueItem);
assertThat(reconstructedItem).isEqualTo(item);
}
}
...@@ -18,127 +18,69 @@ package com.google.android.exoplayer2.ext.cast; ...@@ -18,127 +18,69 @@ package com.google.android.exoplayer2.ext.cast;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Test for {@link MediaItem}. */ /** Test for {@link MediaItem}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public class MediaItemTest { public class MediaItemTest {
@Test @Test
public void buildMediaItem_resetsUuid() {
MediaItem.Builder builder = new MediaItem.Builder();
UUID uuid = new UUID(1, 1);
MediaItem item1 = builder.setUuid(uuid).build();
MediaItem item2 = builder.build();
MediaItem item3 = builder.build();
assertThat(item1.uuid).isEqualTo(uuid);
assertThat(item2.uuid).isNotEqualTo(uuid);
assertThat(item3.uuid).isNotEqualTo(item2.uuid);
assertThat(item3.uuid).isNotEqualTo(uuid);
}
@Test
public void buildMediaItem_doesNotChangeState() { public void buildMediaItem_doesNotChangeState() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item1 = MediaItem item1 =
builder builder
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("http://example.com"))
.setMedia("http://example.com")
.setTitle("title") .setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4) .setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.build(); .build();
MediaItem item2 = builder.setUuid(new UUID(0, 1)).build(); MediaItem item2 = builder.build();
assertThat(item1).isEqualTo(item2); assertThat(item1).isEqualTo(item2);
} }
@Test @Test
public void buildMediaItem_assertDefaultValues() {
assertDefaultValues(new MediaItem.Builder().build());
}
@Test
public void buildAndClear_assertDefaultValues() {
MediaItem.Builder builder = new MediaItem.Builder();
builder
.setMedia("http://example.com")
.setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.buildAndClear();
assertDefaultValues(builder.build());
}
@Test
public void equals_withEqualDrmSchemes_returnsTrue() { public void equals_withEqualDrmSchemes_returnsTrue() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 = MediaItem mediaItem1 =
builder builder1
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear(); MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 = MediaItem mediaItem2 =
builder builder2
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear();
assertThat(mediaItem1).isEqualTo(mediaItem2); assertThat(mediaItem1).isEqualTo(mediaItem2);
} }
@Test @Test
public void equals_withDifferentDrmRequestHeaders_returnsFalse() { public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 = MediaItem mediaItem1 =
builder builder1
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear(); MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 = MediaItem mediaItem2 =
builder builder2
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(2))
.setDrmSchemes(createDummyDrmSchemes(2)) .build();
.buildAndClear();
assertThat(mediaItem1).isNotEqualTo(mediaItem2); assertThat(mediaItem1).isNotEqualTo(mediaItem2);
} }
private static void assertDefaultValues(MediaItem item) { private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
assertThat(item.title).isEmpty(); HashMap<String, String> requestHeaders = new HashMap<>();
assertThat(item.description).isEmpty(); requestHeaders.put("key1", "value1");
assertThat(item.media.uri).isEqualTo(Uri.EMPTY); requestHeaders.put("key2", "value2" + seed);
assertThat(item.attachment).isNull(); return new MediaItem.DrmConfiguration(
assertThat(item.drmSchemes).isEmpty(); C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.mimeType).isEmpty();
}
private static List<MediaItem.DrmScheme> createDummyDrmSchemes(int seed) {
HashMap<String, String> requestHeaders1 = new HashMap<>();
requestHeaders1.put("key1", "value1");
requestHeaders1.put("key2", "value1");
MediaItem.UriBundle uriBundle1 =
new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1);
MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);
HashMap<String, String> requestHeaders2 = new HashMap<>();
requestHeaders2.put("key3", "value3");
requestHeaders2.put("key4", "valueWithSeed" + seed);
MediaItem.UriBundle uriBundle2 =
new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2);
MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);
return Arrays.asList(drmScheme1, drmScheme2);
} }
} }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
## Getting the extension ## ## Getting the extension ##
...@@ -52,4 +52,4 @@ respectively. ...@@ -52,4 +52,4 @@ respectively.
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html
...@@ -16,10 +16,9 @@ apply plugin: 'com.android.library' ...@@ -16,10 +16,9 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
...@@ -27,14 +26,18 @@ android { ...@@ -27,14 +26,18 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
api 'org.chromium.net:cronet-embedded:66.3359.158' api 'org.chromium.net:cronet-embedded:76.3809.111'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -16,7 +16,8 @@ ...@@ -16,7 +16,8 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import android.content.Context; import android.content.Context;
import android.support.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
...@@ -37,8 +38,8 @@ public final class CronetEngineWrapper { ...@@ -37,8 +38,8 @@ public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper"; private static final String TAG = "CronetEngineWrapper";
private final CronetEngine cronetEngine; @Nullable private final CronetEngine cronetEngine;
private final @CronetEngineSource int cronetEngineSource; @CronetEngineSource private final int cronetEngineSource;
/** /**
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link * Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
...@@ -144,7 +145,8 @@ public final class CronetEngineWrapper { ...@@ -144,7 +145,8 @@ public final class CronetEngineWrapper {
* *
* @return A {@link CronetEngineSource} value. * @return A {@link CronetEngineSource} value.
*/ */
public @CronetEngineSource int getCronetEngineSource() { @CronetEngineSource
public int getCronetEngineSource() {
return cronetEngineSource; return cronetEngineSource;
} }
...@@ -153,13 +155,14 @@ public final class CronetEngineWrapper { ...@@ -153,13 +155,14 @@ public final class CronetEngineWrapper {
* *
* @return The CronetEngine, or null if no CronetEngine is available. * @return The CronetEngine, or null if no CronetEngine is available.
*/ */
@Nullable
/* package */ CronetEngine getCronetEngine() { /* package */ CronetEngine getCronetEngine() {
return cronetEngine; return cronetEngine;
} }
private static class CronetProviderComparator implements Comparator<CronetProvider> { private static class CronetProviderComparator implements Comparator<CronetProvider> {
private final String gmsCoreCronetName; @Nullable private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet; private final boolean preferGMSCoreCronet;
// Multi-catch can only be used for API 19+ in this case. // Multi-catch can only be used for API 19+ in this case.
......
/*
* Copyright (C) 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.
*/
@NonNullApi
package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.ext.cronet"/> <manifest package="com.google.android.exoplayer2.ext.cronet">
<uses-sdk/>
</manifest>
...@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
...@@ -28,10 +29,9 @@ import org.junit.Test; ...@@ -28,10 +29,9 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link ByteArrayUploadDataProvider}. */ /** Tests for {@link ByteArrayUploadDataProvider}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public final class ByteArrayUploadDataProviderTest { public final class ByteArrayUploadDataProviderTest {
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
......
...@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately. ...@@ -11,7 +11,7 @@ more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE [Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
## Build instructions ## ## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's its modules locally. Instructions for doing this can be found in ExoPlayer's
...@@ -21,16 +21,15 @@ for more information). ...@@ -21,16 +21,15 @@ for more information).
In addition, it's necessary to build the extension's native components as In addition, it's necessary to build the extension's native components as
follows: follows:
* Set the following environment variables: * Set the following shell variable:
``` ```
cd "<path to exoplayer checkout>" cd "<path to exoplayer checkout>"
EXOPLAYER_ROOT="$(pwd)" FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
``` ```
* Download the [Android NDK][] and set its location in an environment variable. * Download the [Android NDK][] and set its location in a shell variable.
Only versions up to NDK 15c are supported currently. This build configuration has been tested on NDK r20.
``` ```
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
...@@ -42,102 +41,60 @@ NDK_PATH="<path to Android NDK>" ...@@ -42,102 +41,60 @@ NDK_PATH="<path to Android NDK>"
HOST_PLATFORM="linux-x86_64" HOST_PLATFORM="linux-x86_64"
``` ```
* Fetch and build FFmpeg. The configuration flags determine which formats will * Configure the formats supported by adapting the following variable if needed
be supported. See the [Supported formats][] page for more details of the and by setting it. See the [Supported formats][] page for more details of the
available flags. formats.
For example, to fetch and build FFmpeg release 4.0 for armeabi-v7a, ```
arm64-v8a and x86 on Linux x86_64: ENABLED_DECODERS=(vorbis opus flac)
```
* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will
fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86:
``` ```
COMMON_OPTIONS="\ cd "${FFMPEG_EXT_PATH}" && \
--target-os=android \ ./build_ffmpeg.sh \
--disable-static \ "${FFMPEG_EXT_PATH}" "${NDK_PATH}" "${HOST_PLATFORM}" "${ENABLED_DECODERS[@]}"
--enable-shared \
--disable-doc \
--disable-programs \
--disable-everything \
--disable-avdevice \
--disable-avformat \
--disable-swscale \
--disable-postproc \
--disable-avfilter \
--disable-symver \
--disable-swresample \
--enable-avresample \
--enable-decoder=vorbis \
--enable-decoder=opus \
--enable-decoder=flac \
" && \
cd "${FFMPEG_EXT_PATH}/jni" && \
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
cd ffmpeg && git checkout release/4.0 && \
./configure \
--libdir=android-libs/armeabi-v7a \
--arch=arm \
--cpu=armv7-a \
--cross-prefix="${NDK_PATH}/toolchains/arm-linux-androideabi-4.9/prebuilt/${HOST_PLATFORM}/bin/arm-linux-androideabi-" \
--sysroot="${NDK_PATH}/platforms/android-9/arch-arm/" \
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
--extra-ldflags="-Wl,--fix-cortex-a8" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS} \
&& \
make -j4 && make install-libs && \
make clean && ./configure \
--libdir=android-libs/arm64-v8a \
--arch=aarch64 \
--cpu=armv8-a \
--cross-prefix="${NDK_PATH}/toolchains/aarch64-linux-android-4.9/prebuilt/${HOST_PLATFORM}/bin/aarch64-linux-android-" \
--sysroot="${NDK_PATH}/platforms/android-21/arch-arm64/" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS} \
&& \
make -j4 && make install-libs && \
make clean && ./configure \
--libdir=android-libs/x86 \
--arch=x86 \
--cpu=i686 \
--cross-prefix="${NDK_PATH}/toolchains/x86-4.9/prebuilt/${HOST_PLATFORM}/bin/i686-linux-android-" \
--sysroot="${NDK_PATH}/platforms/android-9/arch-x86/" \
--extra-ldexeflags=-pie \
--disable-asm \
${COMMON_OPTIONS} \
&& \
make -j4 && make install-libs && \
make clean
``` ```
* Build the JNI native libraries, setting `APP_ABI` to include the architectures * Build the JNI native libraries, setting `APP_ABI` to include the architectures
built in the previous step. For example: built in the previous step. For example:
``` ```
cd "${FFMPEG_EXT_PATH}"/jni && \ cd "${FFMPEG_EXT_PATH}" && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4
``` ```
## Build instructions (Windows) ##
We do not provide support for building this extension on Windows, however it
should be possible to follow the Linux instructions in [Windows PowerShell][].
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Using the extension ## ## Using the extension ##
Once you've followed the instructions above to check out, build and depend on Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`. the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`.
How you do this depends on which player API you're using: How you do this depends on which player API you're using:
* If you're passing a `DefaultRenderersFactory` to * If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
`ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by you can enable using the extension by setting the `extensionRendererMode`
setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` parameter of the `DefaultRenderersFactory` constructor to
constructor to `EXTENSION_RENDERER_MODE_ON`. This will use `EXTENSION_RENDERER_MODE_ON`. This will use `FfmpegAudioRenderer` for playback
`FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't if `MediaCodecAudioRenderer` doesn't support the input format. Pass
support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `EXTENSION_RENDERER_MODE_PREFER` to give `FfmpegAudioRenderer` priority over
`FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`. `MediaCodecAudioRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer` * If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer`
to the output list in `buildAudioRenderers`. ExoPlayer will use the first to the output list in `buildAudioRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format. `Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return an * If you've implemented your own `RenderersFactory`, return an
`FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the `FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format. first `Renderer` in the returned array that supports the input media format.
* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer` * If you're using `ExoPlayer.Builder`, pass an `FfmpegAudioRenderer` in the
in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
list that supports the input media format. supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation, a custom track selector the choice of `Renderer` is up to your implementation,
...@@ -147,11 +104,21 @@ then implement your own logic to use the renderer for a given track. ...@@ -147,11 +104,21 @@ then implement your own logic to use the renderer for a given track.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[#2781]: https://github.com/google/ExoPlayer/issues/2781 [#2781]: https://github.com/google/ExoPlayer/issues/2781
[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension [Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
## Using the extension in the demo application ##
To try out playback using the extension in the [demo application][], see
[enabling extension decoders][].
[demo application]: https://exoplayer.dev/demo-application.html
[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
## Links ## ## Links ##
* [Troubleshooting using extensions][]
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
belong to this module. belong to this module.
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html [Troubleshooting using extensions]: https://exoplayer.dev/troubleshooting.html#how-can-i-get-a-decoding-extension-to-load-and-be-used-for-playback
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
...@@ -16,7 +16,6 @@ apply plugin: 'com.android.library' ...@@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
...@@ -33,12 +32,16 @@ android { ...@@ -33,12 +32,16 @@ android {
jniLibs.srcDir 'src/main/libs' jniLibs.srcDir 'src/main/libs'
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
No preview for this file type
No preview for this file type
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