Commit eb458555 by sr90 Committed by GitHub

Merge pull request #1 from google/dev-v2

Dev v2 - 1
parents ec6604b4 8967dd9c
Showing with 1941 additions and 646 deletions
...@@ -8,9 +8,12 @@ assignees: '' ...@@ -8,9 +8,12 @@ assignees: ''
Before filing a bug: Before filing a bug:
----------------------- -----------------------
- Search existing issues, including issues that are closed. - Search existing issues, including issues that are closed:
- Consult our FAQs, supported devices and supported formats pages. These can be https://github.com/google/ExoPlayer/issues?q=is%3Aissue
found at https://exoplayer.dev/. - 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 - 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 reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
demo app can be found here: demo app can be found here:
...@@ -33,16 +36,17 @@ or a small sample app that you’re able to share as source code on GitHub. ...@@ -33,16 +36,17 @@ or a small sample app that you’re able to share as source code on GitHub.
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to 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 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 submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
in the format "Issue #1234". Provide all the metadata we'd need to play the in the format "Issue #1234", where "#1234" should be replaced with your issue
content like drm license urls or similar. If the content is accessible only in number. Provide all the metadata we'd need to play the content like drm license
certain countries or regions, please say so. 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 ### [REQUIRED] A full bug report captured from the device
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a 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. 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 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 bug report to dev.exoplayer@gmail.com using a subject in the format
"Issue #1234". "Issue #1234", where "#1234" should be replaced with your issue number.
### [REQUIRED] Version of ExoPlayer being used ### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest". Specify the absolute version number. Avoid using terms such as "latest".
......
...@@ -8,9 +8,12 @@ assignees: '' ...@@ -8,9 +8,12 @@ assignees: ''
Before filing a content issue: Before filing a content issue:
------------------------------ ------------------------------
- Search existing issues, including issues that are closed. - Search existing issues, including issues that are closed:
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- Consult our supported formats page, which can be found at - Consult our supported formats page, which can be found at
https://exoplayer.dev/supported-formats.html. https://exoplayer.dev/supported-formats.html.
- Learn how to create useful log output by using the EventLogger:
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
- Try playing your content in the ExoPlayer demo app. Information about the - Try playing your content in the ExoPlayer demo app. Information about the
ExoPlayer demo app can be found here: ExoPlayer demo app can be found here:
http://exoplayer.dev/demo-application.html. http://exoplayer.dev/demo-application.html.
...@@ -30,9 +33,10 @@ and you expect to play, like 5.1 audio track, text tracks or drm systems. ...@@ -30,9 +33,10 @@ and you expect to play, like 5.1 audio track, text tracks or drm systems.
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to 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 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 submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
in the format "Issue #1234". Provide all the metadata we'd need to play the in the format "Issue #1234", where "#1234" should be replaced with your issue
content like drm license urls or similar. If the content is accessible only in number. Provide all the metadata we'd need to play the content like drm license
certain countries or regions, please say so. urls or similar. If the content is accessible only in certain countries or
regions, please say so.
### [REQUIRED] Version of ExoPlayer being used ### [REQUIRED] Version of ExoPlayer being used
Specify the absolute version number. Avoid using terms such as "latest". Specify the absolute version number. Avoid using terms such as "latest".
...@@ -41,6 +45,13 @@ Specify the absolute version number. Avoid using terms such as "latest". ...@@ -41,6 +45,13 @@ Specify the absolute version number. Avoid using terms such as "latest".
Specify the devices and versions of Android on which you expect the content to Specify the devices and versions of Android on which you expect the content to
play. If possible, please test on multiple devices and Android versions. play. If possible, please test on multiple devices and Android versions.
### [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.
<!-- DO NOT DELETE <!-- DO NOT DELETE
validate_template=true validate_template=true
template_path=.github/ISSUE_TEMPLATE/content_not_playing.md template_path=.github/ISSUE_TEMPLATE/content_not_playing.md
......
...@@ -8,8 +8,9 @@ assignees: '' ...@@ -8,8 +8,9 @@ assignees: ''
Before filing a feature request: Before filing a feature request:
----------------------- -----------------------
- Search existing open issues, specifically with the label ‘enhancement’. - Search existing open issues, specifically with the label ‘enhancement’:
- Search existing pull requests. https://github.com/google/ExoPlayer/labels/enhancement
- Search existing pull requests: https://github.com/google/ExoPlayer/pulls
When filing a feature request: When filing a feature request:
----------------------- -----------------------
......
...@@ -12,8 +12,12 @@ Before filing a question: ...@@ -12,8 +12,12 @@ Before filing a question:
a general Android development question, please do so on Stack Overflow. a general Android development question, please do so on Stack Overflow.
- Search existing issues, including issues that are closed. It’s often the - Search existing issues, including issues that are closed. It’s often the
quickest way to get an answer! quickest way to get an answer!
- Consult our FAQs, developer guide and the class reference of ExoPlayer. These https://github.com/google/ExoPlayer/issues?q=is%3Aissue
can be found at https://exoplayer.dev/. - 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: When filing a question:
----------------------- -----------------------
...@@ -28,6 +32,23 @@ important for us to know this so that we can improve our documentation. ...@@ -28,6 +32,23 @@ important for us to know this so that we can improve our documentation.
### [REQUIRED] Question ### [REQUIRED] Question
Describe your question in detail. 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.
<!-- DO NOT DELETE <!-- DO NOT DELETE
validate_template=true validate_template=true
template_path=.github/ISSUE_TEMPLATE/question.md template_path=.github/ISSUE_TEMPLATE/question.md
......
...@@ -2,18 +2,135 @@ ...@@ -2,18 +2,135 @@
### dev-v2 (not yet released) ### ### dev-v2 (not yet released) ###
* Update `DefaultTrackSelector` to apply a viewport constraint for the default
display by default.
* Add `PlaybackStatsListener` to collect `PlaybackStats` for playbacks analysis
and analytics reporting (TODO: link to developer guide page/blog post).
* Add basic DRM support to the Cast demo app.
* Assume that encrypted content requires secure decoders in renderer support * Assume that encrypted content requires secure decoders in renderer support
checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)). checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)).
* Decoders: Prefer decoders that advertise format support over ones that do not, * Decoders: Prefer decoders that advertise format support over ones that do not,
even if they are listed lower in the `MediaCodecList`. even if they are listed lower in the `MediaCodecList`.
* CEA-608: Handle XDS and TEXT modes
([5807](https://github.com/google/ExoPlayer/pull/5807)).
* Audio: fix an issue where not all audio was played out when the configuration
for the underlying track was changing (e.g., at some period transitions).
* UI: Change playback controls toggle from touch down to touch up events
([#5784](https://github.com/google/ExoPlayer/issues/5784)).
* Add a workaround for broken raw audio decoding on Oppo R9 * Add a workaround for broken raw audio decoding on Oppo R9
([#5782](https://github.com/google/ExoPlayer/issues/5782)). ([#5782](https://github.com/google/ExoPlayer/issues/5782)).
* Add VR player demo.
* Wrap decoder exceptions in a new `DecoderException` class and report as
renderer error.
* Do not pass the manifest to callbacks of `Player.EventListener` and
`SourceInfoRefreshListener` anymore. Instead make it accessible through
`Player.getCurrentManifest()` and `Timeline.Window.manifest`. Also rename
`SourceInfoRefreshListener` to `MediaSourceCaller`.
* Set `compileSdkVersion` to 29 to use Android Q APIs.
* Add `enable` and `disable` methods to `MediaSource` to improve resource
management in playlists.
* Improve text selection logic to always prefer the better language matches
over other selection parameters.
* Remove `AnalyticsCollector.Factory`. Instances can be created directly and
the `Player` set later using `AnalyticsCollector.setPlayer`.
* Add `allowAudioMixedChannelCountAdaptiveness` parameter to
`DefaultTrackSelector` to allow adaptive selections of audio tracks with
different channel counts
([#6257](https://github.com/google/ExoPlayer/issues/6257)).
### 2.10.4 ###
* Offline: Add `Scheduler` implementation that uses `WorkManager`.
* Add ability to specify a description when creating notification channels via
ExoPlayer library classes.
* Switch normalized BCP-47 language codes to use 2-letter ISO 639-1 language
tags instead of 3-letter ISO 639-2 language tags.
* Ensure the `SilenceMediaSource` position is in range
([#6229](https://github.com/google/ExoPlayer/issues/6229)).
* WAV: Calculate correct duration for clipped streams
([#6241](https://github.com/google/ExoPlayer/issues/6241)).
* MP3: Use CBR header bitrate, not calculated bitrate. This reverts a change
from 2.9.3 ([#6238](https://github.com/google/ExoPlayer/issues/6238)).
* Flac extension: Parse `VORBIS_COMMENT` and `PICTURE` metadata
([#5527](https://github.com/google/ExoPlayer/issues/5527)).
* Fix issue where initial seek positions get ignored when playing a preroll ad
([#6201](https://github.com/google/ExoPlayer/issues/6201)).
* Fix issue where invalid language tags were normalized to "und" instead of
keeping the original
([#6153](https://github.com/google/ExoPlayer/issues/6153)).
* Fix `DataSchemeDataSource` re-opening and range requests
([#6192](https://github.com/google/ExoPlayer/issues/6192)).
* Fix Flac and ALAC playback on some LG devices
([#5938](https://github.com/google/ExoPlayer/issues/5938)).
* Fix issue when calling `performClick` on `PlayerView` without
`PlayerControlView`
([#6260](https://github.com/google/ExoPlayer/issues/6260)).
* Fix issue where playback speeds are not used in adaptive track selections
after manual selection changes for other renderers
([#6256](https://github.com/google/ExoPlayer/issues/6256)).
### 2.10.3 ###
* Display last frame when seeking to end of stream
([#2568](https://github.com/google/ExoPlayer/issues/2568)).
* Audio:
* Fix an issue where not all audio was played out when the configuration
for the underlying track was changing (e.g., at some period transitions).
* Fix an issue where playback speed was applied inaccurately in playlists
([#6117](https://github.com/google/ExoPlayer/issues/6117)).
* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is
attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)).
* CEA608: Fix repetition of special North American characters
([#6133](https://github.com/google/ExoPlayer/issues/6133)).
* FLV: Fix bug that caused playback of some live streams to not start
([#6111](https://github.com/google/ExoPlayer/issues/6111)).
* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`.
* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming
playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)).
### 2.10.2 ###
* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s
([#5779](https://github.com/google/ExoPlayer/issues/5779)).
* Add `SilenceMediaSource` that can be used to play silence of a given
duration ([#5735](https://github.com/google/ExoPlayer/issues/5735)).
* Offline:
* Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after
preparation of a `DownloadHelper` fails
([#5915](https://github.com/google/ExoPlayer/issues/5915)).
* Fix `CacheUtil.cache()` downloading too much data
([#5927](https://github.com/google/ExoPlayer/issues/5927)).
* Fix misreporting cached bytes when caching is paused
([#5573](https://github.com/google/ExoPlayer/issues/5573)).
* UI:
* Allow setting `DefaultTimeBar` attributes on `PlayerView` and
`PlayerControlView`.
* Change playback controls toggle from touch down to touch up events
([#5784](https://github.com/google/ExoPlayer/issues/5784)).
* Fix issue where playback controls were not kept visible on key presses
([#5963](https://github.com/google/ExoPlayer/issues/5963)).
* Subtitles:
* CEA-608: Handle XDS and TEXT modes
([#5807](https://github.com/google/ExoPlayer/pull/5807)).
* TTML: Fix bitmap rendering
([#5633](https://github.com/google/ExoPlayer/pull/5633)).
* IMA: Fix ad pod index offset calculation without preroll
([#5928](https://github.com/google/ExoPlayer/issues/5928)).
* Add a `playWhenReady` flag to MediaSessionConnector.PlaybackPreparer methods
to indicate whether a controller sent a play or only a prepare command. This
allows to take advantage of decoder reuse with the MediaSessionConnector
([#5891](https://github.com/google/ExoPlayer/issues/5891)).
* Add `ProgressUpdateListener` to `PlayerControlView`
([#5834](https://github.com/google/ExoPlayer/issues/5834)).
* Add support for auto-detecting UDP streams in `DefaultDataSource`
([#6036](https://github.com/google/ExoPlayer/pull/6036)).
* Allow enabling decoder fallback with `DefaultRenderersFactory`
([#5942](https://github.com/google/ExoPlayer/issues/5942)).
* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission
([#6019](https://github.com/google/ExoPlayer/issues/6019)).
* Fix decoding problems when seeking back after seeking beyond a mid-roll ad
([#6009](https://github.com/google/ExoPlayer/issues/6009)).
* Fix application of `maxAudioBitrate` for adaptive audio track groups
([#6006](https://github.com/google/ExoPlayer/issues/6006)).
* Fix bug caused by parallel adaptive track selection using `Format`s without
bitrate information
([#5971](https://github.com/google/ExoPlayer/issues/5971)).
* Fix bug in `CastPlayer.getCurrentWindowIndex()`
([#5955](https://github.com/google/ExoPlayer/issues/5955)).
### 2.10.1 ### ### 2.10.1 ###
......
...@@ -21,14 +21,6 @@ buildscript { ...@@ -21,14 +21,6 @@ buildscript {
classpath 'com.novoda:bintray-release:0.9' classpath 'com.novoda:bintray-release:0.9'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.0' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.1.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 {
repositories { repositories {
...@@ -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,17 +13,18 @@ ...@@ -13,17 +13,18 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.10.1' releaseVersion = '2.10.4'
releaseVersionCode = 2010001 releaseVersionCode = 2010004
minSdkVersion = 16 minSdkVersion = 16
targetSdkVersion = 28 targetSdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 29
dexmakerVersion = '2.21.0' dexmakerVersion = '2.21.0'
mockitoVersion = '2.25.0' mockitoVersion = '2.25.0'
robolectricVersion = '4.3-alpha-2' robolectricVersion = '4.3'
autoValueVersion = '1.6' autoValueVersion = '1.6'
autoServiceVersion = '1.0-rc4' autoServiceVersion = '1.0-rc4'
checkerframeworkVersion = '2.5.0' checkerframeworkVersion = '2.5.0'
jsr305Version = '3.0.2'
androidXTestVersion = '1.1.0' androidXTestVersion = '1.1.0'
truthVersion = '0.44' truthVersion = '0.44'
modulePrefix = ':' modulePrefix = ':'
......
...@@ -24,7 +24,6 @@ include modulePrefix + 'library-hls' ...@@ -24,7 +24,6 @@ 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-ffmpeg' include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac' include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-gvr'
...@@ -38,6 +37,7 @@ include modulePrefix + 'extension-vp9' ...@@ -38,6 +37,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 +46,6 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl ...@@ -46,7 +46,6 @@ 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-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 +59,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio ...@@ -60,3 +59,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')
...@@ -47,17 +47,6 @@ android { ...@@ -47,17 +47,6 @@ android {
// The demo app isn't indexed and doesn't have translations. // The demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation' disable 'GoogleAppIndexingWarning','MissingTranslation'
} }
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
} }
dependencies { dependencies {
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
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,87 +16,86 @@ ...@@ -16,87 +16,86 @@
package com.google.android.exoplayer2.castdemo; package com.google.android.exoplayer2.castdemo;
import android.net.Uri; import android.net.Uri;
import androidx.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. // 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(
new MediaItem.Builder()
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
.setTitle("Clear HLS: Angel one")
.setMimeType(MIME_TYPE_HLS)
.build());
samples.add(
new MediaItem.Builder()
.setUri("https://html5demos.com/assets/dizzy.mp4")
.setTitle("Clear MP4: Dizzy")
.setMimeType(MIME_TYPE_VIDEO_MP4)
.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(
new MediaItem.Builder()
.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( 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/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);
} }
......
...@@ -17,6 +17,8 @@ package com.google.android.exoplayer2.castdemo; ...@@ -17,6 +17,8 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils; import androidx.core.graphics.ColorUtils;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
...@@ -39,11 +41,9 @@ import com.google.android.exoplayer2.SimpleExoPlayer; ...@@ -39,11 +41,9 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.dynamite.DynamiteModule; import com.google.android.gms.dynamite.DynamiteModule;
import java.util.Collections;
/** /**
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
...@@ -52,8 +52,6 @@ import java.util.Collections; ...@@ -52,8 +52,6 @@ import java.util.Collections;
public class MainActivity extends AppCompatActivity public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.Listener { implements OnClickListener, PlayerManager.Listener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView; private PlayerView localPlayerView;
private PlayerControlView castControlView; private PlayerControlView castControlView;
private PlayerManager playerManager; private PlayerManager playerManager;
...@@ -61,10 +59,6 @@ public class MainActivity extends AppCompatActivity ...@@ -61,10 +59,6 @@ public class MainActivity extends AppCompatActivity
private MediaQueueListAdapter mediaQueueListAdapter; private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext; private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods. // Activity lifecycle methods.
@Override @Override
...@@ -118,20 +112,13 @@ public class MainActivity extends AppCompatActivity ...@@ -118,20 +112,13 @@ public class MainActivity extends AppCompatActivity
// There is no Cast context to work with. Do nothing. // There is no Cast context to work with. Do nothing.
return; return;
} }
String applicationId = castContext.getCastOptions().getReceiverApplicationId(); playerManager =
switch (applicationId) { new PlayerManager(
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: /* listener= */ this,
playerManager = localPlayerView,
new DefaultReceiverPlayerManager( castControlView,
/* listener= */ this, /* context= */ this,
localPlayerView, castContext);
castControlView,
/* context= */ this,
castContext);
break;
default:
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
}
mediaQueueList.setAdapter(mediaQueueListAdapter); mediaQueueList.setAdapter(mediaQueueListAdapter);
} }
...@@ -179,36 +166,29 @@ public class MainActivity extends AppCompatActivity ...@@ -179,36 +166,29 @@ public class MainActivity extends AppCompatActivity
} }
@Override @Override
public void onQueueContentsExternallyChanged() { public void onUnsupportedTrack(int trackType) {
mediaQueueListAdapter.notifyDataSetChanged(); if (trackType == C.TRACK_TYPE_AUDIO) {
} showToast(R.string.error_unsupported_audio);
} else if (trackType == C.TRACK_TYPE_VIDEO) {
@Override showToast(R.string.error_unsupported_video);
public void onPlayerError() { } else {
Toast.makeText(getApplicationContext(), R.string.player_error_msg, Toast.LENGTH_LONG).show(); // Do nothing.
}
} }
// Internal methods. // Internal methods.
private void showToast(int messageId) {
Toast.makeText(getApplicationContext(), messageId, Toast.LENGTH_LONG).show();
}
private View buildSampleListView() { private View buildSampleListView() {
View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null);
ListView sampleList = dialogList.findViewById(R.id.sample_list); ListView sampleList = dialogList.findViewById(R.id.sample_list);
sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener( sampleList.setOnItemClickListener(
(parent, view, position, id) -> { (parent, view, position, id) -> {
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); playerManager.addItem(DemoUtil.SAMPLES.get(position));
mediaItemBuilder
.clear()
.setMedia(sample.uri)
.setTitle(sample.name)
.setMimeType(sample.mimeType);
if (sample.drmSchemeUuid != null) {
mediaItemBuilder.setDrmSchemes(
Collections.singletonList(
new MediaItem.DrmScheme(
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
}
playerManager.addItem(mediaItemBuilder.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}); });
return dialogList; return dialogList;
...@@ -231,8 +211,10 @@ public class MainActivity extends AppCompatActivity ...@@ -231,8 +211,10 @@ public class MainActivity extends AppCompatActivity
TextView view = holder.textView; TextView view = holder.textView;
view.setText(holder.item.title); view.setText(holder.item.title);
// TODO: Solve coloring using the theme's ColorStateList. // TODO: Solve coloring using the theme's ColorStateList.
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), view.setTextColor(
position == playerManager.getCurrentItemIndex() ? 255 : 100)); ColorUtils.setAlphaComponent(
view.getCurrentTextColor(),
position == playerManager.getCurrentItemIndex() ? 255 : 100));
} }
@Override @Override
...@@ -312,11 +294,18 @@ public class MainActivity extends AppCompatActivity ...@@ -312,11 +294,18 @@ public class MainActivity extends AppCompatActivity
} }
} }
private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.Sample> { private static final class SampleListAdapter extends ArrayAdapter<MediaItem> {
public SampleListAdapter(Context context) { public SampleListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES);
} }
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = super.getView(position, convertView, parent);
((TextView) view).setText(getItem(position).title);
return view;
}
}
} }
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
<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="player_error_msg">Player error encountered. Select a queue item to reprepare. Check the logcat and receiver app\'s console for more info.</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>
# ExoPlayer VR player demo #
This folder contains a demo application that showcases 360 video playback using
ExoPlayer GVR extension.
// 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,8 +11,8 @@ ...@@ -11,8 +11,8 @@
// 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.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
...@@ -23,24 +23,37 @@ android { ...@@ -23,24 +23,37 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion project.ext.minSdkVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
lintOptions { buildTypes {
// Robolectric depends on BouncyCastle, which depends on javax.naming, release {
// which is not part of Android. shrinkResources true
disable 'InvalidPackage' minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
debug {
jniDebuggable = true
}
} }
testOptions.unitTests.includeAndroidResources = true lintOptions {
// The demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
} }
dependencies { dependencies {
api 'androidx.test:core:' + androidXTestVersion
api 'org.robolectric:robolectric:' + robolectricVersion
api project(modulePrefix + 'testutils')
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation project(modulePrefix + 'library-ui')
annotationProcessor 'com.google.auto.service:auto-service:' + autoServiceVersion implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-gvr')
implementation 'androidx.annotation:annotation:1.1.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
<?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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.gvrdemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:largeHeap="true">
<activity
android:name="com.google.android.exoplayer2.gvrdemo.SampleChooserActivity"
android:configChanges="keyboardHidden"
android:exported="true"
android:label="@string/application_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
<data android:host="*"/>
<data android:pathPattern=".*\\.exolist\\.json"/>
</intent-filter>
</activity>
<activity
android:name="com.google.android.exoplayer2.gvrdemo.PlayerActivity"
android:configChanges="density|keyboardHidden|navigation|orientation|screenSize|uiMode"
android:enableVrMode="@string/gvr_vr_mode_component"
android:exported="false"
android:label="@string/application_name"
android:launchMode="singleTask"
android:resizeableActivity="false"
android:screenOrientation="landscape"
android:theme="@style/VrActivityTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="com.google.intent.category.CARDBOARD"/> <!-- copybara:strip(development-only) -->
<category android:name="com.google.intent.category.DAYDREAM"/>
</intent-filter>
</activity>
</application>
</manifest>
/*
* 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.gvrdemo;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.widget.Toast;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.gvr.GvrPlayerActivity;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
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.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.Util;
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends GvrPlayerActivity implements PlaybackPreparer {
public static final String EXTENSION_EXTRA = "extension";
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector;
private TrackGroupArray lastSeenTrackGroupArray;
private boolean startAutoPlay;
private int startWindow;
private long startPosition;
// Activity lifecycle
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
dataSourceFactory =
new DefaultDataSourceFactory(this, new DefaultHttpDataSourceFactory(userAgent));
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
if (sphericalStereoMode != null) {
int stereoMode;
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_MONO;
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
} else {
showToast(R.string.error_unrecognized_stereo_mode);
finish();
return;
}
setDefaultStereoMode(stereoMode);
}
clearStartPosition();
}
@Override
public void onResume() {
super.onResume();
if (Util.SDK_INT <= 23 || player == null) {
initializePlayer();
}
}
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
releasePlayer();
}
}
@Override
public void onDestroy() {
super.onDestroy();
}
// PlaybackControlView.PlaybackPreparer implementation
@Override
public void preparePlayback() {
initializePlayer();
}
// Internal methods
private void initializePlayer() {
if (player == null) {
Intent intent = getIntent();
Uri uri = intent.getData();
if (!Util.checkCleartextTrafficPermitted(uri)) {
showToast(R.string.error_cleartext_not_permitted);
return;
}
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this);
trackSelector = new DefaultTrackSelector(/* context= */ this);
lastSeenTrackGroupArray = null;
player =
ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector));
setPlayer(player);
mediaSource = buildMediaSource(uri, intent.getStringExtra(EXTENSION_EXTRA));
}
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
if (haveStartPosition) {
player.seekTo(startWindow, startPosition);
}
player.prepare(mediaSource, !haveStartPosition, false);
}
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
@ContentType int type = Util.inferContentType(uri, overrideExtension);
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 ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
private void releasePlayer() {
if (player != null) {
updateStartPosition();
player.release();
player = null;
mediaSource = null;
trackSelector = null;
}
}
private void updateStartPosition() {
if (player != null) {
startAutoPlay = player.getPlayWhenReady();
startWindow = player.getCurrentWindowIndex();
startPosition = Math.max(0, player.getContentPosition());
}
}
private void clearStartPosition() {
startAutoPlay = true;
startWindow = C.INDEX_UNSET;
startPosition = C.TIME_UNSET;
}
private void showToast(int messageId) {
showToast(getString(messageId));
}
private void showToast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
private class PlayerEventListener implements Player.EventListener {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
if (player.getPlaybackError() != null) {
// The user has performed a seek whilst in the error state. Update the resume position so
// that if the user then retries, playback resumes from the position to which they seeked.
updateStartPosition();
}
}
@Override
public void onPlayerError(ExoPlaybackException e) {
updateStartPosition();
}
@Override
@SuppressWarnings("ReferenceEquality")
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
if (trackGroups != lastSeenTrackGroupArray) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_video);
}
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio);
}
}
lastSeenTrackGroupArray = trackGroups;
}
}
}
}
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.gvrdemo;
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_LEFT_RIGHT;
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_MONO;
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_TOP_BOTTOM;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
/** An activity for selecting from a list of media samples. */
public class SampleChooserActivity extends Activity {
private final Sample[] samples =
new Sample[] {
new Sample(
"Congo (360 top-bottom stereo)",
"https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"Sphericalv2 (180 top-bottom stereo)",
"https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"Iceland (360 top-bottom stereo ts)",
"https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"Camera motion metadata test",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/synthetic_with_camm.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"actual_camera_cat",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/actual_camera_cat.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"johnny_stitched",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/johnny_stitched.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"lenovo_birds.vr",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/lenovo_birds.vr.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"mono_v1_sample",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/mono_v1_sample.mp4",
SPHERICAL_STEREO_MODE_MONO),
new Sample(
"not_vr180_actually_shot_with_moto_mod",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/"
+ "not_vr180_actually_shot_with_moto_mod.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"stereo_v1_sample",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/stereo_v1_sample.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
new Sample(
"yi_giraffes.vr",
"https://storage.googleapis.com/exoplayer-test-media-internal-"
+ "63834241aced7884c2544af1a3452e01/vr180/yi_giraffes.vr.mp4",
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sample_chooser_activity);
ListView sampleListView = findViewById(R.id.sample_list);
sampleListView.setAdapter(
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, samples));
sampleListView.setOnItemClickListener(
(parent, view, position, id) ->
startActivity(
samples[position].buildIntent(/* context= */ SampleChooserActivity.this)));
}
private static final class Sample {
public final String name;
public final String uri;
public final String extension;
public final String sphericalStereoMode;
public Sample(String name, String uri, String sphericalStereoMode) {
this(name, uri, sphericalStereoMode, null);
}
public Sample(String name, String uri, String sphericalStereoMode, String extension) {
this.name = name;
this.uri = uri;
this.extension = extension;
this.sphericalStereoMode = sphericalStereoMode;
}
public Intent buildIntent(Context context) {
Intent intent = new Intent(context, PlayerActivity.class);
return intent
.setData(Uri.parse(uri))
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
}
@Override
public String toString() {
return name;
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView android:id="@+id/sample_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
<?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.
...@@ -13,5 +13,16 @@ ...@@ -13,5 +13,16 @@
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.
--> -->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<manifest package="com.google.android.exoplayer2.testutil"/> <string name="application_name">ExoPlayer VR Demo</string>
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</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>
...@@ -53,7 +53,7 @@ dependencies { ...@@ -53,7 +53,7 @@ dependencies {
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima') implementation project(modulePrefix + 'extension-ima')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
...@@ -62,7 +62,7 @@ android { ...@@ -62,7 +62,7 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.viewpager:viewpager:1.0.0' implementation 'androidx.viewpager:viewpager:1.0.0'
implementation 'androidx.fragment:fragment:1.0.0' implementation 'androidx.fragment:fragment:1.0.0'
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.0.0'
......
...@@ -41,7 +41,8 @@ public class DemoDownloadService extends DownloadService { ...@@ -41,7 +41,8 @@ public class DemoDownloadService extends DownloadService {
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;
} }
......
...@@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadIndex; ...@@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
...@@ -55,6 +56,7 @@ public class DownloadTracker { ...@@ -55,6 +56,7 @@ public class DownloadTracker {
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, Download> downloads; private final HashMap<Uri, Download> downloads;
private final DownloadIndex downloadIndex; private final DownloadIndex downloadIndex;
private final DefaultTrackSelector.Parameters trackSelectorParameters;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
...@@ -65,6 +67,7 @@ public class DownloadTracker { ...@@ -65,6 +67,7 @@ public class DownloadTracker {
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
downloads = new HashMap<>(); downloads = new HashMap<>();
downloadIndex = downloadManager.getDownloadIndex(); downloadIndex = downloadManager.getDownloadIndex();
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
downloadManager.addListener(new DownloadManagerListener()); downloadManager.addListener(new DownloadManagerListener());
loadDownloads(); loadDownloads();
} }
...@@ -123,13 +126,13 @@ public class DownloadTracker { ...@@ -123,13 +126,13 @@ public class DownloadTracker {
int type = Util.inferContentType(uri, extension); int type = Util.inferContentType(uri, extension);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS: case C.TYPE_SS:
return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS: case C.TYPE_HLS:
return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return DownloadHelper.forProgressive(uri); return DownloadHelper.forProgressive(context, uri);
default: default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }
...@@ -202,7 +205,7 @@ public class DownloadTracker { ...@@ -202,7 +205,7 @@ public class DownloadTracker {
TrackSelectionDialog.createForMappedTrackInfoAndParameters( TrackSelectionDialog.createForMappedTrackInfoAndParameters(
/* titleId= */ R.string.exo_download_description, /* titleId= */ R.string.exo_download_description,
mappedTrackInfo, mappedTrackInfo,
/* initialParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters,
/* allowAdaptiveSelections =*/ false, /* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true, /* allowMultipleOverrides= */ true,
/* onClickListener= */ this, /* onClickListener= */ this,
...@@ -212,9 +215,7 @@ public class DownloadTracker { ...@@ -212,9 +215,7 @@ public class DownloadTracker {
@Override @Override
public void onPrepareError(DownloadHelper helper, IOException e) { public void onPrepareError(DownloadHelper helper, IOException e) {
Toast.makeText( Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Failed to start download", e); Log.e(TAG, "Failed to start download", e);
} }
...@@ -229,7 +230,7 @@ public class DownloadTracker { ...@@ -229,7 +230,7 @@ public class DownloadTracker {
downloadHelper.addTrackSelectionForSingleRenderer( downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex, periodIndex,
/* rendererIndex= */ i, /* rendererIndex= */ i,
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
} }
} }
......
/*
* 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.demo;
import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.UUID;
/* package */ abstract class Sample {
public static final class UriSample extends Sample {
public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
return new UriSample(
/* name= */ null,
DrmInfo.createFromIntent(intent, extrasKeySuffix),
uri,
extension,
adTagUri,
/* sphericalStereoMode= */ null);
}
public final Uri uri;
public final String extension;
public final DrmInfo drmInfo;
public final Uri adTagUri;
public final String sphericalStereoMode;
public UriSample(
String name,
DrmInfo drmInfo,
Uri uri,
String extension,
Uri adTagUri,
String sphericalStereoMode) {
super(name);
this.uri = uri;
this.extension = extension;
this.drmInfo = drmInfo;
this.adTagUri = adTagUri;
this.sphericalStereoMode = sphericalStereoMode;
}
@Override
public void addToIntent(Intent intent) {
intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
}
public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
addPlayerConfigToIntent(intent, extrasKeySuffix);
}
private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
intent
.putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
.putExtra(
AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
if (drmInfo != null) {
drmInfo.addToIntent(intent, extrasKeySuffix);
}
}
}
public static final class PlaylistSample extends Sample {
public final UriSample[] children;
public PlaylistSample(String name, UriSample... children) {
super(name);
this.children = children;
}
@Override
public void addToIntent(Intent intent) {
intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
for (int i = 0; i < children.length; i++) {
children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
}
}
}
public static final class DrmInfo {
public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
return null;
}
String drmSchemeExtra =
intent.hasExtra(schemeKey)
? intent.getStringExtra(schemeKey)
: intent.getStringExtra(schemeUuidKey);
UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
String[] keyRequestPropertiesArray =
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
boolean drmMultiSession =
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
return new DrmInfo(drmScheme, drmLicenseUrl, keyRequestPropertiesArray, drmMultiSession);
}
public final UUID drmScheme;
public final String drmLicenseUrl;
public final String[] drmKeyRequestProperties;
public final boolean drmMultiSession;
public DrmInfo(
UUID drmScheme,
String drmLicenseUrl,
String[] drmKeyRequestProperties,
boolean drmMultiSession) {
this.drmScheme = drmScheme;
this.drmLicenseUrl = drmLicenseUrl;
this.drmKeyRequestProperties = drmKeyRequestProperties;
this.drmMultiSession = drmMultiSession;
}
public void addToIntent(Intent intent, String extrasKeySuffix) {
Assertions.checkNotNull(intent);
intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
}
}
public static Sample createFromIntent(Intent intent) {
if (ACTION_VIEW_LIST.equals(intent.getAction())) {
ArrayList<String> intentUris = new ArrayList<>();
int index = 0;
while (intent.hasExtra(URI_EXTRA + "_" + index)) {
intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
index++;
}
UriSample[] children = new UriSample[intentUris.size()];
for (int i = 0; i < children.length; i++) {
Uri uri = Uri.parse(intentUris.get(i));
children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
}
return new PlaylistSample(/* name= */ null, children);
} else {
return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
}
}
@Nullable public final String name;
public Sample(String name) {
this.name = name;
}
public abstract void addToIntent(Intent intent);
}
...@@ -38,6 +38,9 @@ import android.widget.TextView; ...@@ -38,6 +38,9 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.demo.Sample.DrmInfo;
import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSourceInputStream;
...@@ -161,13 +164,17 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -161,13 +164,17 @@ public class SampleChooserActivity extends AppCompatActivity
public boolean onChildClick( public boolean onChildClick(
ExpandableListView parent, View view, int groupPosition, int childPosition, long id) { ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
Sample sample = (Sample) view.getTag(); Sample sample = (Sample) view.getTag();
startActivity( Intent intent = new Intent(this, PlayerActivity.class);
sample.buildIntent( intent.putExtra(
/* context= */ this, PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
isNonNullAndChecked(preferExtensionDecodersMenuItem), isNonNullAndChecked(preferExtensionDecodersMenuItem));
isNonNullAndChecked(randomAbrMenuItem) String abrAlgorithm =
? PlayerActivity.ABR_ALGORITHM_RANDOM isNonNullAndChecked(randomAbrMenuItem)
: PlayerActivity.ABR_ALGORITHM_DEFAULT)); ? PlayerActivity.ABR_ALGORITHM_RANDOM
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
sample.addToIntent(intent);
startActivity(intent);
return true; return true;
} }
...@@ -309,17 +316,12 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -309,17 +316,12 @@ public class SampleChooserActivity extends AppCompatActivity
extension = reader.nextString(); extension = reader.nextString();
break; break;
case "drm_scheme": case "drm_scheme":
Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme");
drmScheme = reader.nextString(); drmScheme = reader.nextString();
break; break;
case "drm_license_url": case "drm_license_url":
Assertions.checkState(!insidePlaylist,
"Invalid attribute on nested item: drm_license_url");
drmLicenseUrl = reader.nextString(); drmLicenseUrl = reader.nextString();
break; break;
case "drm_key_request_properties": case "drm_key_request_properties":
Assertions.checkState(!insidePlaylist,
"Invalid attribute on nested item: drm_key_request_properties");
ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>(); ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
reader.beginObject(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
...@@ -357,17 +359,21 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -357,17 +359,21 @@ public class SampleChooserActivity extends AppCompatActivity
DrmInfo drmInfo = DrmInfo drmInfo =
drmScheme == null drmScheme == null
? null ? null
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession); : new DrmInfo(
Util.getDrmUuid(drmScheme),
drmLicenseUrl,
drmKeyRequestProperties,
drmMultiSession);
if (playlistSamples != null) { if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); return new PlaylistSample(sampleName, playlistSamplesArray);
} else { } else {
return new UriSample( return new UriSample(
sampleName, sampleName,
drmInfo, drmInfo,
uri, uri,
extension, extension,
adTagUri, adTagUri != null ? Uri.parse(adTagUri) : null,
sphericalStereoMode); sphericalStereoMode);
} }
} }
...@@ -497,116 +503,4 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -497,116 +503,4 @@ public class SampleChooserActivity extends AppCompatActivity
} }
} }
private static final class DrmInfo {
public final String drmScheme;
public final String drmLicenseUrl;
public final String[] drmKeyRequestProperties;
public final boolean drmMultiSession;
public DrmInfo(
String drmScheme,
String drmLicenseUrl,
String[] drmKeyRequestProperties,
boolean drmMultiSession) {
this.drmScheme = drmScheme;
this.drmLicenseUrl = drmLicenseUrl;
this.drmKeyRequestProperties = drmKeyRequestProperties;
this.drmMultiSession = drmMultiSession;
}
public void updateIntent(Intent intent) {
Assertions.checkNotNull(intent);
intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmScheme);
intent.putExtra(PlayerActivity.DRM_LICENSE_URL_EXTRA, drmLicenseUrl);
intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA, drmKeyRequestProperties);
intent.putExtra(PlayerActivity.DRM_MULTI_SESSION_EXTRA, drmMultiSession);
}
}
private abstract static class Sample {
public final String name;
public final DrmInfo drmInfo;
public Sample(String name, DrmInfo drmInfo) {
this.name = name;
this.drmInfo = drmInfo;
}
public Intent buildIntent(
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
Intent intent = new Intent(context, PlayerActivity.class);
intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA, preferExtensionDecoders);
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
if (drmInfo != null) {
drmInfo.updateIntent(intent);
}
return intent;
}
}
private static final class UriSample extends Sample {
public final Uri uri;
public final String extension;
public final String adTagUri;
public final String sphericalStereoMode;
public UriSample(
String name,
DrmInfo drmInfo,
Uri uri,
String extension,
String adTagUri,
String sphericalStereoMode) {
super(name, drmInfo);
this.uri = uri;
this.extension = extension;
this.adTagUri = adTagUri;
this.sphericalStereoMode = sphericalStereoMode;
}
@Override
public Intent buildIntent(
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
.setData(uri)
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
.putExtra(PlayerActivity.AD_TAG_URI_EXTRA, adTagUri)
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode)
.setAction(PlayerActivity.ACTION_VIEW);
}
}
private static final class PlaylistSample extends Sample {
public final UriSample[] children;
public PlaylistSample(
String name,
DrmInfo drmInfo,
UriSample... children) {
super(name, drmInfo);
this.children = children;
}
@Override
public Intent buildIntent(
Context context, boolean preferExtensionDecoders, String abrAlgorithm) {
String[] uris = new String[children.length];
String[] extensions = new String[children.length];
for (int i = 0; i < children.length; i++) {
uris[i] = children[i].uri.toString();
extensions[i] = children[i].extension;
}
return super.buildIntent(context, preferExtensionDecoders, abrAlgorithm)
.putExtra(PlayerActivity.URI_LIST_EXTRA, uris)
.putExtra(PlayerActivity.EXTENSION_LIST_EXTRA, extensions)
.setAction(PlayerActivity.ACTION_VIEW_LIST);
}
}
} }
...@@ -306,7 +306,7 @@ public final class TrackSelectionDialog extends DialogFragment { ...@@ -306,7 +306,7 @@ public final class TrackSelectionDialog extends DialogFragment {
} }
} }
/** Fragment to show a track seleciton in tab of the track selection dialog. */ /** Fragment to show a track selection in tab of the track selection dialog. */
public static final class TrackSelectionViewFragment extends Fragment public static final class TrackSelectionViewFragment extends Fragment
implements TrackSelectionView.TrackSelectionListener { implements TrackSelectionView.TrackSelectionListener {
......
...@@ -53,6 +53,8 @@ ...@@ -53,6 +53,8 @@
<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="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
<string name="download_playlist_unsupported">This demo app does not support downloading playlists</string> <string name="download_playlist_unsupported">This demo app does not support downloading playlists</string>
......
...@@ -31,13 +31,14 @@ android { ...@@ -31,13 +31,14 @@ android {
} }
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'
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -117,6 +117,7 @@ import java.util.Arrays; ...@@ -117,6 +117,7 @@ import java.util.Arrays;
Object tag = setTag ? ids[windowIndex] : null; Object tag = setTag ? ids[windowIndex] : null;
return window.set( return window.set(
tag, tag,
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic, /* isSeekable= */ !isDynamic,
......
...@@ -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;
...@@ -33,7 +34,7 @@ import com.google.android.gms.cast.MediaTrack; ...@@ -33,7 +34,7 @@ import com.google.android.gms.cast.MediaTrack;
* @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, or {@link C#TIME_UNSET} if unknown or not applicable. * @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) {
if (mediaInfo == null) { if (mediaInfo == null) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
......
...@@ -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 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);
}
}
...@@ -21,10 +21,7 @@ import android.net.Uri; ...@@ -21,10 +21,7 @@ import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.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;
...@@ -33,112 +30,57 @@ import org.junit.runner.RunWith; ...@@ -33,112 +30,57 @@ import org.junit.runner.RunWith;
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);
} }
} }
...@@ -31,11 +31,13 @@ android { ...@@ -31,11 +31,13 @@ android {
} }
dependencies { dependencies {
api 'org.chromium.net:cronet-embedded:73.3683.76' api 'org.chromium.net:cronet-embedded:75.3770.101'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
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 {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cronet;
import android.content.Context; import android.content.Context;
import androidx.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>
...@@ -38,9 +38,10 @@ android { ...@@ -38,9 +38,10 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -92,8 +92,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -92,8 +92,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager, protected int supportsFormatInternal(
Format format) { @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
Assertions.checkNotNull(format.sampleMimeType); Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable()) { if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
...@@ -113,7 +113,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -113,7 +113,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException { throws FfmpegDecoderException {
int initialInputBufferSize = int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
......
...@@ -42,7 +42,7 @@ import java.util.List; ...@@ -42,7 +42,7 @@ import java.util.List;
private static final int DECODER_ERROR_OTHER = -2; private static final int DECODER_ERROR_OTHER = -2;
private final String codecName; private final String codecName;
private final @Nullable byte[] extraData; @Nullable private final byte[] extraData;
private final @C.Encoding int encoding; private final @C.Encoding int encoding;
private final int outputBufferSize; private final int outputBufferSize;
...@@ -172,28 +172,49 @@ import java.util.List; ...@@ -172,28 +172,49 @@ import java.util.List;
private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) { private static @Nullable byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_ALAC:
case MimeTypes.AUDIO_OPUS: case MimeTypes.AUDIO_OPUS:
return initializationData.get(0); return initializationData.get(0);
case MimeTypes.AUDIO_ALAC:
return getAlacExtraData(initializationData);
case MimeTypes.AUDIO_VORBIS: case MimeTypes.AUDIO_VORBIS:
byte[] header0 = initializationData.get(0); return getVorbisExtraData(initializationData);
byte[] header1 = initializationData.get(1);
byte[] extraData = new byte[header0.length + header1.length + 6];
extraData[0] = (byte) (header0.length >> 8);
extraData[1] = (byte) (header0.length & 0xFF);
System.arraycopy(header0, 0, extraData, 2, header0.length);
extraData[header0.length + 2] = 0;
extraData[header0.length + 3] = 0;
extraData[header0.length + 4] = (byte) (header1.length >> 8);
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
return extraData;
default: default:
// Other codecs do not require extra data. // Other codecs do not require extra data.
return null; return null;
} }
} }
private static byte[] getAlacExtraData(List<byte[]> initializationData) {
// FFmpeg's ALAC decoder expects an ALAC atom, which contains the ALAC "magic cookie", as extra
// data. initializationData[0] contains only the magic cookie, and so we need to package it into
// an ALAC atom. See:
// https://ffmpeg.org/doxygen/0.6/alac_8c.html
// https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt
byte[] magicCookie = initializationData.get(0);
int alacAtomLength = 12 + magicCookie.length;
ByteBuffer alacAtom = ByteBuffer.allocate(alacAtomLength);
alacAtom.putInt(alacAtomLength);
alacAtom.putInt(0x616c6163); // type=alac
alacAtom.putInt(0); // version=0, flags=0
alacAtom.put(magicCookie, /* offset= */ 0, magicCookie.length);
return alacAtom.array();
}
private static byte[] getVorbisExtraData(List<byte[]> initializationData) {
byte[] header0 = initializationData.get(0);
byte[] header1 = initializationData.get(1);
byte[] extraData = new byte[header0.length + header1.length + 6];
extraData[0] = (byte) (header0.length >> 8);
extraData[1] = (byte) (header0.length & 0xFF);
System.arraycopy(header0, 0, extraData, 2, header0.length);
extraData[header0.length + 2] = 0;
extraData[header0.length + 3] = 0;
extraData[header0.length + 4] = (byte) (header1.length >> 8);
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
return extraData;
}
private native long ffmpegInitialize( private native long ffmpegInitialize(
String codecName, String codecName,
@Nullable byte[] extraData, @Nullable byte[] extraData,
......
/*
* 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.ffmpeg;
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.ffmpeg"/> <manifest package="com.google.android.exoplayer2.ext.ffmpeg">
<uses-sdk/>
</manifest>
...@@ -39,10 +39,12 @@ android { ...@@ -39,10 +39,12 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -9,6 +9,9 @@ ...@@ -9,6 +9,9 @@
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*; *;
} }
-keep class com.google.android.exoplayer2.util.FlacStreamInfo { -keep class com.google.android.exoplayer2.util.FlacStreamMetadata {
*;
}
-keep class com.google.android.exoplayer2.metadata.flac.PictureFrame {
*; *;
} }
...@@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest { ...@@ -52,7 +52,10 @@ public final class FlacBinarySearchSeekerTest {
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
SeekMap seekMap = seeker.getSeekMap(); SeekMap seekMap = seeker.getSeekMap();
assertThat(seekMap).isNotNull(); assertThat(seekMap).isNotNull();
...@@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest { ...@@ -70,7 +73,10 @@ public final class FlacBinarySearchSeekerTest {
decoderJni.setData(input); decoderJni.setData(input);
FlacBinarySearchSeeker seeker = FlacBinarySearchSeeker seeker =
new FlacBinarySearchSeeker( new FlacBinarySearchSeeker(
decoderJni.decodeMetadata(), /* firstFramePosition= */ 0, data.length, decoderJni); decoderJni.decodeStreamMetadata(),
/* firstFramePosition= */ 0,
data.length,
decoderJni);
seeker.setSeekTargetUs(/* timeUs= */ 1000); seeker.setSeekTargetUs(/* timeUs= */ 1000);
assertThat(seeker.isSeeking()).isTrue(); assertThat(seeker.isSeeking()).isTrue();
......
...@@ -228,7 +228,8 @@ public final class FlacExtractorSeekTest { ...@@ -228,7 +228,8 @@ public final class FlacExtractorSeekTest {
} }
} }
private @Nullable SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output) @Nullable
private SeekMap extractSeekMap(FlacExtractor extractor, FakeExtractorOutput output)
throws IOException, InterruptedException { throws IOException, InterruptedException {
try { try {
ExtractorInput input = getExtractorInputFromPosition(0); ExtractorInput input = getExtractorInputFromPosition(0);
......
...@@ -28,7 +28,7 @@ import org.junit.runner.RunWith; ...@@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
public class FlacExtractorTest { public class FlacExtractorTest {
@Before @Before
public void setUp() throws Exception { public void setUp() {
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
fail("Flac library not available."); fail("Flac library not available.");
} }
......
...@@ -82,7 +82,7 @@ public class FlacPlaybackTest { ...@@ -82,7 +82,7 @@ public class FlacPlaybackTest {
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(); DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
MediaSource mediaSource = MediaSource mediaSource =
......
...@@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker; ...@@ -19,7 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -34,20 +34,20 @@ import java.nio.ByteBuffer; ...@@ -34,20 +34,20 @@ import java.nio.ByteBuffer;
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
public FlacBinarySearchSeeker( public FlacBinarySearchSeeker(
FlacStreamInfo streamInfo, FlacStreamMetadata streamMetadata,
long firstFramePosition, long firstFramePosition,
long inputLength, long inputLength,
FlacDecoderJni decoderJni) { FlacDecoderJni decoderJni) {
super( super(
new FlacSeekTimestampConverter(streamInfo), new FlacSeekTimestampConverter(streamMetadata),
new FlacTimestampSeeker(decoderJni), new FlacTimestampSeeker(decoderJni),
streamInfo.durationUs(), streamMetadata.durationUs(),
/* floorTimePosition= */ 0, /* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamInfo.totalSamples, /* ceilingTimePosition= */ streamMetadata.totalSamples,
/* floorBytePosition= */ firstFramePosition, /* floorBytePosition= */ firstFramePosition,
/* ceilingBytePosition= */ inputLength, /* ceilingBytePosition= */ inputLength,
/* approxBytesPerFrame= */ streamInfo.getApproxBytesPerFrame(), /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(),
/* minimumSearchRange= */ Math.max(1, streamInfo.minFrameSize)); /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize));
this.decoderJni = Assertions.checkNotNull(decoderJni); this.decoderJni = Assertions.checkNotNull(decoderJni);
} }
...@@ -112,15 +112,15 @@ import java.nio.ByteBuffer; ...@@ -112,15 +112,15 @@ import java.nio.ByteBuffer;
* the timestamp for a stream seek time position. * the timestamp for a stream seek time position.
*/ */
private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { private static final class FlacSeekTimestampConverter implements SeekTimestampConverter {
private final FlacStreamInfo streamInfo; private final FlacStreamMetadata streamMetadata;
public FlacSeekTimestampConverter(FlacStreamInfo streamInfo) { public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) {
this.streamInfo = streamInfo; this.streamMetadata = streamMetadata;
} }
@Override @Override
public long timeUsToTargetTime(long timeUs) { public long timeUsToTargetTime(long timeUs) {
return Assertions.checkNotNull(streamInfo).getSampleIndex(timeUs); return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs);
} }
} }
} }
...@@ -15,11 +15,13 @@ ...@@ -15,11 +15,13 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
...@@ -56,21 +58,20 @@ import java.util.List; ...@@ -56,21 +58,20 @@ import java.util.List;
} }
decoderJni = new FlacDecoderJni(); decoderJni = new FlacDecoderJni();
decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0)));
FlacStreamInfo streamInfo; FlacStreamMetadata streamMetadata;
try { try {
streamInfo = decoderJni.decodeMetadata(); streamMetadata = decoderJni.decodeStreamMetadata();
} catch (ParserException e) {
throw new FlacDecoderException("Failed to decode StreamInfo", e);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// Never happens. // Never happens.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
if (streamInfo == null) {
throw new FlacDecoderException("Metadata decoding failed");
}
int initialInputBufferSize = int initialInputBufferSize =
maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamInfo.maxFrameSize; maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize;
setInitialInputBufferSize(initialInputBufferSize); setInitialInputBufferSize(initialInputBufferSize);
maxOutputBufferSize = streamInfo.maxDecodedFrameSize(); maxOutputBufferSize = streamMetadata.maxDecodedFrameSize();
} }
@Override @Override
...@@ -94,6 +95,7 @@ import java.util.List; ...@@ -94,6 +95,7 @@ import java.util.List;
} }
@Override @Override
@Nullable
protected FlacDecoderException decode( protected FlacDecoderException decode(
DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) { if (reset) {
......
...@@ -15,9 +15,12 @@ ...@@ -15,9 +15,12 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.FlacStreamMetadata;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -37,14 +40,14 @@ import java.nio.ByteBuffer; ...@@ -37,14 +40,14 @@ import java.nio.ByteBuffer;
} }
} }
private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size which libflac has private static final int TEMP_BUFFER_SIZE = 8192; // The same buffer size as libflac.
private final long nativeDecoderContext; private final long nativeDecoderContext;
private ByteBuffer byteBufferData; @Nullable private ByteBuffer byteBufferData;
private ExtractorInput extractorInput; @Nullable private ExtractorInput extractorInput;
@Nullable private byte[] tempBuffer;
private boolean endOfExtractorInput; private boolean endOfExtractorInput;
private byte[] tempBuffer;
public FlacDecoderJni() throws FlacDecoderException { public FlacDecoderJni() throws FlacDecoderException {
if (!FlacLibrary.isAvailable()) { if (!FlacLibrary.isAvailable()) {
...@@ -57,67 +60,79 @@ import java.nio.ByteBuffer; ...@@ -57,67 +60,79 @@ import java.nio.ByteBuffer;
} }
/** /**
* Sets data to be parsed by libflac. * Sets the data to be parsed.
* @param byteBufferData Source {@link ByteBuffer} *
* @param byteBufferData Source {@link ByteBuffer}.
*/ */
public void setData(ByteBuffer byteBufferData) { public void setData(ByteBuffer byteBufferData) {
this.byteBufferData = byteBufferData; this.byteBufferData = byteBufferData;
this.extractorInput = null; this.extractorInput = null;
this.tempBuffer = null;
} }
/** /**
* Sets data to be parsed by libflac. * Sets the data to be parsed.
* @param extractorInput Source {@link ExtractorInput} *
* @param extractorInput Source {@link ExtractorInput}.
*/ */
public void setData(ExtractorInput extractorInput) { public void setData(ExtractorInput extractorInput) {
this.byteBufferData = null; this.byteBufferData = null;
this.extractorInput = extractorInput; this.extractorInput = extractorInput;
endOfExtractorInput = false;
if (tempBuffer == null) { if (tempBuffer == null) {
this.tempBuffer = new byte[TEMP_BUFFER_SIZE]; tempBuffer = new byte[TEMP_BUFFER_SIZE];
} }
endOfExtractorInput = false;
} }
/**
* Returns whether the end of the data to be parsed has been reached, or true if no data was set.
*/
public boolean isEndOfData() { public boolean isEndOfData() {
if (byteBufferData != null) { if (byteBufferData != null) {
return byteBufferData.remaining() == 0; return byteBufferData.remaining() == 0;
} else if (extractorInput != null) { } else if (extractorInput != null) {
return endOfExtractorInput; return endOfExtractorInput;
} else {
return true;
} }
return true; }
/** Clears the data to be parsed. */
public void clearData() {
byteBufferData = null;
extractorInput = null;
} }
/** /**
* Reads up to {@code length} bytes from the data source. * Reads up to {@code length} bytes from the data source.
* <p> *
* This method blocks until at least one byte of data can be read, the end of the input is * <p>This method blocks until at least one byte of data can be read, the end of the input is
* detected or an exception is thrown. * detected or an exception is thrown.
* <p>
* This method is called from the native code.
* *
* @param target A target {@link ByteBuffer} into which data should be written. * @param target A target {@link ByteBuffer} into which data should be written.
* @return Returns the number of bytes read, or -1 on failure. It's not an error if this returns * @return Returns the number of bytes read, or -1 on failure. If all of the data has already been
* zero; it just means all the data read from the source. * read from the source, then 0 is returned.
*/ */
@SuppressWarnings("unused") // Called from native code.
public int read(ByteBuffer target) throws IOException, InterruptedException { public int read(ByteBuffer target) throws IOException, InterruptedException {
int byteCount = target.remaining(); int byteCount = target.remaining();
if (byteBufferData != null) { if (byteBufferData != null) {
byteCount = Math.min(byteCount, byteBufferData.remaining()); byteCount = Math.min(byteCount, byteBufferData.remaining());
int originalLimit = byteBufferData.limit(); int originalLimit = byteBufferData.limit();
byteBufferData.limit(byteBufferData.position() + byteCount); byteBufferData.limit(byteBufferData.position() + byteCount);
target.put(byteBufferData); target.put(byteBufferData);
byteBufferData.limit(originalLimit); byteBufferData.limit(originalLimit);
} else if (extractorInput != null) { } else if (extractorInput != null) {
ExtractorInput extractorInput = this.extractorInput;
byte[] tempBuffer = Util.castNonNull(this.tempBuffer);
byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE); byteCount = Math.min(byteCount, TEMP_BUFFER_SIZE);
int read = readFromExtractorInput(0, byteCount); int read = readFromExtractorInput(extractorInput, tempBuffer, /* offset= */ 0, byteCount);
if (read < 4) { if (read < 4) {
// Reading less than 4 bytes, most of the time, happens because of getting the bytes left in // Reading less than 4 bytes, most of the time, happens because of getting the bytes left in
// the buffer of the input. Do another read to reduce the number of calls to this method // the buffer of the input. Do another read to reduce the number of calls to this method
// from the native code. // from the native code.
read += readFromExtractorInput(read, byteCount - read); read +=
readFromExtractorInput(
extractorInput, tempBuffer, read, /* length= */ byteCount - read);
} }
byteCount = read; byteCount = read;
target.put(tempBuffer, 0, byteCount); target.put(tempBuffer, 0, byteCount);
...@@ -127,9 +142,13 @@ import java.nio.ByteBuffer; ...@@ -127,9 +142,13 @@ import java.nio.ByteBuffer;
return byteCount; return byteCount;
} }
/** Decodes and consumes the StreamInfo section from the FLAC stream. */ /** Decodes and consumes the metadata from the FLAC stream. */
public FlacStreamInfo decodeMetadata() throws IOException, InterruptedException { public FlacStreamMetadata decodeStreamMetadata() throws IOException, InterruptedException {
return flacDecodeMetadata(nativeDecoderContext); FlacStreamMetadata streamMetadata = flacDecodeMetadata(nativeDecoderContext);
if (streamMetadata == null) {
throw new ParserException("Failed to decode stream metadata");
}
return streamMetadata;
} }
/** /**
...@@ -234,7 +253,8 @@ import java.nio.ByteBuffer; ...@@ -234,7 +253,8 @@ import java.nio.ByteBuffer;
flacRelease(nativeDecoderContext); flacRelease(nativeDecoderContext);
} }
private int readFromExtractorInput(int offset, int length) private int readFromExtractorInput(
ExtractorInput extractorInput, byte[] tempBuffer, int offset, int length)
throws IOException, InterruptedException { throws IOException, InterruptedException {
int read = extractorInput.read(tempBuffer, offset, length); int read = extractorInput.read(tempBuffer, offset, length);
if (read == C.RESULT_END_OF_INPUT) { if (read == C.RESULT_END_OF_INPUT) {
...@@ -246,7 +266,7 @@ import java.nio.ByteBuffer; ...@@ -246,7 +266,7 @@ import java.nio.ByteBuffer;
private native long flacInit(); private native long flacInit();
private native FlacStreamInfo flacDecodeMetadata(long context) private native FlacStreamMetadata flacDecodeMetadata(long context)
throws IOException, InterruptedException; throws IOException, InterruptedException;
private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer) private native int flacDecodeToBuffer(long context, ByteBuffer outputBuffer)
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import android.os.Handler; import android.os.Handler;
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.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioProcessor;
...@@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -33,7 +34,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
private static final int NUM_BUFFERS = 16; private static final int NUM_BUFFERS = 16;
public LibflacAudioRenderer() { public LibflacAudioRenderer() {
this(null, null); this(/* eventHandler= */ null, /* eventListener= */ null);
} }
/** /**
...@@ -43,15 +44,15 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -43,15 +44,15 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/ */
public LibflacAudioRenderer( public LibflacAudioRenderer(
Handler eventHandler, @Nullable Handler eventHandler,
AudioRendererEventListener eventListener, @Nullable AudioRendererEventListener eventListener,
AudioProcessor... audioProcessors) { AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors); super(eventHandler, eventListener, audioProcessors);
} }
@Override @Override
protected int supportsFormatInternal(DrmSessionManager<ExoMediaCrypto> drmSessionManager, protected int supportsFormatInternal(
Format format) { @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
if (!FlacLibrary.isAvailable() if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
...@@ -65,7 +66,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -65,7 +66,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws FlacDecoderException { throws FlacDecoderException {
return new FlacDecoder( return new FlacDecoder(
NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData);
......
/*
* 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.flac;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -14,9 +14,12 @@ ...@@ -14,9 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
#include <jni.h>
#include <android/log.h> #include <android/log.h>
#include <jni.h>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include "include/flac_parser.h" #include "include/flac_parser.h"
#define LOG_TAG "flac_jni" #define LOG_TAG "flac_jni"
...@@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { ...@@ -95,19 +98,68 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) {
return NULL; return NULL;
} }
jclass arrayListClass = env->FindClass("java/util/ArrayList");
jmethodID arrayListConstructor =
env->GetMethodID(arrayListClass, "<init>", "()V");
jobject commentList = env->NewObject(arrayListClass, arrayListConstructor);
jmethodID arrayListAddMethod =
env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
if (context->parser->areVorbisCommentsValid()) {
std::vector<std::string> vorbisComments =
context->parser->getVorbisComments();
for (std::vector<std::string>::const_iterator vorbisComment =
vorbisComments.begin();
vorbisComment != vorbisComments.end(); ++vorbisComment) {
jstring commentString = env->NewStringUTF((*vorbisComment).c_str());
env->CallBooleanMethod(commentList, arrayListAddMethod, commentString);
env->DeleteLocalRef(commentString);
}
}
jobject pictureFrames = env->NewObject(arrayListClass, arrayListConstructor);
bool picturesValid = context->parser->arePicturesValid();
if (picturesValid) {
std::vector<FlacPicture> pictures = context->parser->getPictures();
jclass pictureFrameClass = env->FindClass(
"com/google/android/exoplayer2/metadata/flac/PictureFrame");
jmethodID pictureFrameConstructor =
env->GetMethodID(pictureFrameClass, "<init>",
"(ILjava/lang/String;Ljava/lang/String;IIII[B)V");
for (std::vector<FlacPicture>::const_iterator picture = pictures.begin();
picture != pictures.end(); ++picture) {
jstring mimeType = env->NewStringUTF(picture->mimeType.c_str());
jstring description = env->NewStringUTF(picture->description.c_str());
jbyteArray pictureData = env->NewByteArray(picture->data.size());
env->SetByteArrayRegion(pictureData, 0, picture->data.size(),
(signed char *)&picture->data[0]);
jobject pictureFrame = env->NewObject(
pictureFrameClass, pictureFrameConstructor, picture->type, mimeType,
description, picture->width, picture->height, picture->depth,
picture->colors, pictureData);
env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame);
env->DeleteLocalRef(mimeType);
env->DeleteLocalRef(description);
env->DeleteLocalRef(pictureData);
}
}
const FLAC__StreamMetadata_StreamInfo &streamInfo = const FLAC__StreamMetadata_StreamInfo &streamInfo =
context->parser->getStreamInfo(); context->parser->getStreamInfo();
jclass cls = env->FindClass( jclass flacStreamMetadataClass = env->FindClass(
"com/google/android/exoplayer2/util/" "com/google/android/exoplayer2/util/"
"FlacStreamInfo"); "FlacStreamMetadata");
jmethodID constructor = env->GetMethodID(cls, "<init>", "(IIIIIIIJ)V"); jmethodID flacStreamMetadataConstructor =
env->GetMethodID(flacStreamMetadataClass, "<init>",
return env->NewObject(cls, constructor, streamInfo.min_blocksize, "(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
streamInfo.max_blocksize, streamInfo.min_framesize,
streamInfo.max_framesize, streamInfo.sample_rate, return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
streamInfo.channels, streamInfo.bits_per_sample, streamInfo.min_blocksize, streamInfo.max_blocksize,
streamInfo.total_samples); streamInfo.min_framesize, streamInfo.max_framesize,
streamInfo.sample_rate, streamInfo.channels,
streamInfo.bits_per_sample, streamInfo.total_samples,
commentList, pictureFrames);
} }
DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) { DECODER_FUNC(jint, flacDecodeToBuffer, jlong jContext, jobject jOutputBuffer) {
......
...@@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { ...@@ -172,6 +172,43 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) {
case FLAC__METADATA_TYPE_SEEKTABLE: case FLAC__METADATA_TYPE_SEEKTABLE:
mSeekTable = &metadata->data.seek_table; mSeekTable = &metadata->data.seek_table;
break; break;
case FLAC__METADATA_TYPE_VORBIS_COMMENT:
if (!mVorbisCommentsValid) {
FLAC__StreamMetadata_VorbisComment vorbisComment =
metadata->data.vorbis_comment;
for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) {
FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry =
vorbisComment.comments[i];
if (vorbisCommentEntry.entry != NULL) {
std::string comment(
reinterpret_cast<char *>(vorbisCommentEntry.entry),
vorbisCommentEntry.length);
mVorbisComments.push_back(comment);
}
}
mVorbisCommentsValid = true;
} else {
ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT");
}
break;
case FLAC__METADATA_TYPE_PICTURE: {
const FLAC__StreamMetadata_Picture *parsedPicture =
&metadata->data.picture;
FlacPicture picture;
picture.mimeType.assign(std::string(parsedPicture->mime_type));
picture.description.assign(
std::string((char *)parsedPicture->description));
picture.data.assign(parsedPicture->data,
parsedPicture->data + parsedPicture->data_length);
picture.width = parsedPicture->width;
picture.height = parsedPicture->height;
picture.depth = parsedPicture->depth;
picture.colors = parsedPicture->colors;
picture.type = parsedPicture->type;
mPictures.push_back(picture);
mPicturesValid = true;
break;
}
default: default:
ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type);
break; break;
...@@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source) ...@@ -233,6 +270,8 @@ FLACParser::FLACParser(DataSource *source)
mCurrentPos(0LL), mCurrentPos(0LL),
mEOF(false), mEOF(false),
mStreamInfoValid(false), mStreamInfoValid(false),
mVorbisCommentsValid(false),
mPicturesValid(false),
mWriteRequested(false), mWriteRequested(false),
mWriteCompleted(false), mWriteCompleted(false),
mWriteBuffer(NULL), mWriteBuffer(NULL),
...@@ -266,6 +305,10 @@ bool FLACParser::init() { ...@@ -266,6 +305,10 @@ bool FLACParser::init() {
FLAC__METADATA_TYPE_STREAMINFO); FLAC__METADATA_TYPE_STREAMINFO);
FLAC__stream_decoder_set_metadata_respond(mDecoder, FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_SEEKTABLE); FLAC__METADATA_TYPE_SEEKTABLE);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__stream_decoder_set_metadata_respond(mDecoder,
FLAC__METADATA_TYPE_PICTURE);
FLAC__StreamDecoderInitStatus initStatus; FLAC__StreamDecoderInitStatus initStatus;
initStatus = FLAC__stream_decoder_init_stream( initStatus = FLAC__stream_decoder_init_stream(
mDecoder, read_callback, seek_callback, tell_callback, length_callback, mDecoder, read_callback, seek_callback, tell_callback, length_callback,
......
...@@ -19,6 +19,10 @@ ...@@ -19,6 +19,10 @@
#include <stdint.h> #include <stdint.h>
#include <cstdlib>
#include <string>
#include <vector>
// libFLAC parser // libFLAC parser
#include "FLAC/stream_decoder.h" #include "FLAC/stream_decoder.h"
...@@ -26,6 +30,17 @@ ...@@ -26,6 +30,17 @@
typedef int status_t; typedef int status_t;
struct FlacPicture {
int type;
std::string mimeType;
std::string description;
FLAC__uint32 width;
FLAC__uint32 height;
FLAC__uint32 depth;
FLAC__uint32 colors;
std::vector<char> data;
};
class FLACParser { class FLACParser {
public: public:
FLACParser(DataSource *source); FLACParser(DataSource *source);
...@@ -44,6 +59,16 @@ class FLACParser { ...@@ -44,6 +59,16 @@ class FLACParser {
return mStreamInfo; return mStreamInfo;
} }
bool areVorbisCommentsValid() const { return mVorbisCommentsValid; }
const std::vector<std::string>& getVorbisComments() const {
return mVorbisComments;
}
bool arePicturesValid() const { return mPicturesValid; }
const std::vector<FlacPicture> &getPictures() const { return mPictures; }
int64_t getLastFrameTimestamp() const { int64_t getLastFrameTimestamp() const {
return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate(); return (1000000LL * mWriteHeader.number.sample_number) / getSampleRate();
} }
...@@ -71,6 +96,10 @@ class FLACParser { ...@@ -71,6 +96,10 @@ class FLACParser {
mEOF = false; mEOF = false;
if (newPosition == 0) { if (newPosition == 0) {
mStreamInfoValid = false; mStreamInfoValid = false;
mVorbisCommentsValid = false;
mPicturesValid = false;
mVorbisComments.clear();
mPictures.clear();
FLAC__stream_decoder_reset(mDecoder); FLAC__stream_decoder_reset(mDecoder);
} else { } else {
FLAC__stream_decoder_flush(mDecoder); FLAC__stream_decoder_flush(mDecoder);
...@@ -116,6 +145,14 @@ class FLACParser { ...@@ -116,6 +145,14 @@ class FLACParser {
const FLAC__StreamMetadata_SeekTable *mSeekTable; const FLAC__StreamMetadata_SeekTable *mSeekTable;
uint64_t firstFrameOffset; uint64_t firstFrameOffset;
// cached when the VORBIS_COMMENT metadata is parsed by libFLAC
std::vector<std::string> mVorbisComments;
bool mVorbisCommentsValid;
// cached when the PICTURE metadata is parsed by libFLAC
std::vector<FlacPicture> mPictures;
bool mPicturesValid;
// cached when a decoded PCM block is "written" by libFLAC parser // cached when a decoded PCM block is "written" by libFLAC parser
bool mWriteRequested; bool mWriteRequested;
bool mWriteCompleted; bool mWriteCompleted;
......
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.ext.flac"/> <manifest package="com.google.android.exoplayer2.ext.flac">
<uses-sdk/>
</manifest>
...@@ -33,7 +33,7 @@ android { ...@@ -33,7 +33,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
api 'com.google.vr:sdk-base:1.190.0' api 'com.google.vr:sdk-base:1.190.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }
......
...@@ -50,7 +50,10 @@ import com.google.vr.sdk.controller.ControllerManager; ...@@ -50,7 +50,10 @@ import com.google.vr.sdk.controller.ControllerManager;
import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLConfig;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Base activity for VR 360 video playback. */ /**
* Base activity for VR 360 video playback. Before starting the video playback a player needs to be
* set using {@link #setPlayer(Player)}.
*/
public abstract class GvrPlayerActivity extends GvrActivity { public abstract class GvrPlayerActivity extends GvrActivity {
private static final int EXIT_FROM_VR_REQUEST_CODE = 42; private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
...@@ -58,12 +61,12 @@ public abstract class GvrPlayerActivity extends GvrActivity { ...@@ -58,12 +61,12 @@ public abstract class GvrPlayerActivity extends GvrActivity {
private final Handler mainHandler; private final Handler mainHandler;
@Nullable private Player player; @Nullable private Player player;
@MonotonicNonNull private GlViewGroup glView; private @MonotonicNonNull GlViewGroup glView;
@MonotonicNonNull private ControllerManager controllerManager; private @MonotonicNonNull ControllerManager controllerManager;
@MonotonicNonNull private SurfaceTexture surfaceTexture; private @MonotonicNonNull SurfaceTexture surfaceTexture;
@MonotonicNonNull private Surface surface; private @MonotonicNonNull Surface surface;
@MonotonicNonNull private SceneRenderer scene; private @MonotonicNonNull SceneRenderer scene;
@MonotonicNonNull private PlayerControlView playerControl; private @MonotonicNonNull PlayerControlView playerControl;
public GvrPlayerActivity() { public GvrPlayerActivity() {
mainHandler = new Handler(Looper.getMainLooper()); mainHandler = new Handler(Looper.getMainLooper());
......
...@@ -32,10 +32,12 @@ android { ...@@ -32,10 +32,12 @@ android {
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.2' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.11.3'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {
......
...@@ -313,14 +313,14 @@ public final class ImaAdsLoader ...@@ -313,14 +313,14 @@ public final class ImaAdsLoader
*/ */
private static final int IMA_AD_STATE_PAUSED = 2; private static final int IMA_AD_STATE_PAUSED = 2;
private final @Nullable Uri adTagUri; @Nullable private final Uri adTagUri;
private final @Nullable String adsResponse; @Nullable private final String adsResponse;
private final int vastLoadTimeoutMs; private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs; private final int mediaLoadTimeoutMs;
private final boolean focusSkipButtonWhenAvailable; private final boolean focusSkipButtonWhenAvailable;
private final int mediaBitrate; private final int mediaBitrate;
private final @Nullable Set<UiElement> adUiElements; @Nullable private final Set<UiElement> adUiElements;
private final @Nullable AdEventListener adEventListener; @Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory; private final ImaFactory imaFactory;
private final Timeline.Period period; private final Timeline.Period period;
private final List<VideoAdPlayerCallback> adCallbacks; private final List<VideoAdPlayerCallback> adCallbacks;
...@@ -426,7 +426,7 @@ public final class ImaAdsLoader ...@@ -426,7 +426,7 @@ public final class ImaAdsLoader
* @deprecated Use {@link ImaAdsLoader.Builder}. * @deprecated Use {@link ImaAdsLoader.Builder}.
*/ */
@Deprecated @Deprecated
public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { public ImaAdsLoader(Context context, Uri adTagUri, @Nullable ImaSdkSettings imaSdkSettings) {
this( this(
context, context,
adTagUri, adTagUri,
...@@ -946,8 +946,7 @@ public final class ImaAdsLoader ...@@ -946,8 +946,7 @@ public final class ImaAdsLoader
// Player.EventListener implementation. // Player.EventListener implementation.
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
// The player is being reset or contains no media. // The player is being reset or contains no media.
return; return;
...@@ -1054,13 +1053,8 @@ public final class ImaAdsLoader ...@@ -1054,13 +1053,8 @@ public final class ImaAdsLoader
long contentPositionMs = player.getCurrentPosition(); long contentPositionMs = player.getCurrentPosition();
int adGroupIndexForPosition = int adGroupIndexForPosition =
adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
if (adGroupIndexForPosition == 0) { if (adGroupIndexForPosition > 0 && adGroupIndexForPosition != C.INDEX_UNSET) {
podIndexOffset = 0; // Skip any ad groups before the one at or immediately before the playback position.
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
// There is no preroll and midroll pod indices start at 1.
podIndexOffset = -1;
} else /* adGroupIndexForPosition > 0 */ {
// Skip ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) { for (int i = 0; i < adGroupIndexForPosition; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i); adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
} }
...@@ -1070,9 +1064,18 @@ public final class ImaAdsLoader ...@@ -1070,9 +1064,18 @@ public final class ImaAdsLoader
long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1];
double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d;
adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND);
}
// We're removing one or more ads, which means that the earliest ad (if any) will be a // IMA indexes any remaining midroll ad pods from 1. A preroll (if present) has index 0.
// midroll/postroll. Midroll pod indices start at 1. // Store an index offset as we want to index all ads (including skipped ones) from 0.
if (adGroupIndexForPosition == 0 && adGroupTimesUs[0] == 0) {
// We are playing a preroll.
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
// There's no ad to play which means there's no preroll.
podIndexOffset = -1;
} else {
// We are playing a midroll and any ads before it were skipped.
podIndexOffset = adGroupIndexForPosition - 1; podIndexOffset = adGroupIndexForPosition - 1;
} }
......
/*
* 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.ima;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -13,4 +13,6 @@ ...@@ -13,4 +13,6 @@
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.
--> -->
<manifest package="com.google.android.exoplayer2.ext.ima.test" /> <manifest package="com.google.android.exoplayer2.ext.ima.test">
<uses-sdk/>
</manifest>
...@@ -51,9 +51,7 @@ import java.util.ArrayList; ...@@ -51,9 +51,7 @@ import java.util.ArrayList;
public void updateTimeline(Timeline timeline) { public void updateTimeline(Timeline timeline) {
for (Player.EventListener listener : listeners) { for (Player.EventListener listener : listeners) {
listener.onTimelineChanged( listener.onTimelineChanged(
timeline, timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
null,
prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
} }
prepared = true; prepared = true;
} }
......
...@@ -252,7 +252,8 @@ public class ImaAdsLoaderTest { ...@@ -252,7 +252,8 @@ public class ImaAdsLoaderTest {
} }
@Override @Override
public @Nullable Ad getAd() { @Nullable
public Ad getAd() {
return ad; return ad;
} }
......
# ExoPlayer Firebase JobDispatcher extension # # ExoPlayer Firebase JobDispatcher extension #
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
[WorkManager extension]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/workmanager/README.md
[PlatformScheduler]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android [Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android
## Getting the extension ## ## Getting the extension ##
...@@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's ...@@ -20,4 +24,3 @@ locally. Instructions for doing this can be found in ExoPlayer's
[top level README][]. [top level README][].
[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
...@@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util; ...@@ -54,7 +54,10 @@ import com.google.android.exoplayer2.util.Util;
* *
* @see <a * @see <a
* href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a> * href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
* @deprecated Use com.google.android.exoplayer2.ext.workmanager.WorkManagerScheduler or {@link
* com.google.android.exoplayer2.scheduler.PlatformScheduler}.
*/ */
@Deprecated
public final class JobDispatcherScheduler implements Scheduler { public final class JobDispatcherScheduler implements Scheduler {
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
......
/*
* 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.jobdispatcher;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -32,7 +32,7 @@ android { ...@@ -32,7 +32,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.leanback:leanback:1.0.0' implementation 'androidx.leanback:leanback:1.0.0'
} }
......
...@@ -51,10 +51,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab ...@@ -51,10 +51,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
private final ComponentListener componentListener; private final ComponentListener componentListener;
private final int updatePeriodMs; private final int updatePeriodMs;
private @Nullable PlaybackPreparer playbackPreparer; @Nullable private PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher; private ControlDispatcher controlDispatcher;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable SurfaceHolderGlueHost surfaceHolderGlueHost; @Nullable private SurfaceHolderGlueHost surfaceHolderGlueHost;
private boolean hasSurface; private boolean hasSurface;
private boolean lastNotifiedPreparedState; private boolean lastNotifiedPreparedState;
...@@ -288,8 +288,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab ...@@ -288,8 +288,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
} }
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
Callback callback = getCallback(); Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
......
/*
* 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.leanback;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -33,6 +33,7 @@ android { ...@@ -33,6 +33,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
api 'androidx.media:media:1.0.1' api 'androidx.media:media:1.0.1'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
} }
ext { ext {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.mediasession; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.mediasession;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
...@@ -65,7 +66,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus ...@@ -65,7 +66,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus
@Override @Override
public void onCustomAction( public void onCustomAction(
Player player, ControlDispatcher controlDispatcher, String action, Bundle extras) { Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras) {
int mode = player.getRepeatMode(); int mode = player.getRepeatMode();
int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes); int proposedMode = RepeatModeUtil.getNextRepeatMode(mode, repeatToggleModes);
if (mode != proposedMode) { if (mode != proposedMode) {
......
...@@ -166,7 +166,7 @@ public final class TimelineQueueEditor ...@@ -166,7 +166,7 @@ public final class TimelineQueueEditor
@Override @Override
public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) { public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) {
MediaSource mediaSource = sourceFactory.createMediaSource(description); @Nullable MediaSource mediaSource = sourceFactory.createMediaSource(description);
if (mediaSource != null) { if (mediaSource != null) {
queueDataAdapter.add(index, description); queueDataAdapter.add(index, description);
queueMediaSource.addMediaSource(index, mediaSource); queueMediaSource.addMediaSource(index, mediaSource);
......
/*
* 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.mediasession;
import com.google.android.exoplayer2.util.NonNullApi;
...@@ -33,7 +33,7 @@ android { ...@@ -33,7 +33,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.annotation:annotation:1.1.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
api 'com.squareup.okhttp3:okhttp:3.12.1' api 'com.squareup.okhttp3:okhttp:3.12.1'
} }
......
...@@ -57,14 +57,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -57,14 +57,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private final Call.Factory callFactory; private final Call.Factory callFactory;
private final RequestProperties requestProperties; private final RequestProperties requestProperties;
private final @Nullable String userAgent; @Nullable private final String userAgent;
private final @Nullable Predicate<String> contentTypePredicate; @Nullable private final CacheControl cacheControl;
private final @Nullable CacheControl cacheControl; @Nullable private final RequestProperties defaultRequestProperties;
private final @Nullable RequestProperties defaultRequestProperties;
@Nullable private Predicate<String> contentTypePredicate;
private @Nullable DataSpec dataSpec; @Nullable private DataSpec dataSpec;
private @Nullable Response response; @Nullable private Response response;
private @Nullable InputStream responseByteStream; @Nullable private InputStream responseByteStream;
private boolean opened; private boolean opened;
private long bytesToSkip; private long bytesToSkip;
...@@ -79,7 +79,28 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -79,7 +79,28 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param userAgent An optional User-Agent string. * @param userAgent An optional User-Agent string.
*/ */
public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) { public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
this(callFactory, userAgent, /* contentTypePredicate= */ null); this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent An optional User-Agent string.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
* server as HTTP headers on every request.
*/
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@Nullable CacheControl cacheControl,
@Nullable RequestProperties defaultRequestProperties) {
super(/* isNetwork= */ true);
this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = userAgent;
this.cacheControl = cacheControl;
this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
} }
/** /**
...@@ -89,7 +110,10 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -89,7 +110,10 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from {@link * predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}. * #open(DataSpec)}.
* @deprecated Use {@link #OkHttpDataSource(Call.Factory, String)} and {@link
* #setContentTypePredicate(Predicate)}.
*/ */
@Deprecated
public OkHttpDataSource( public OkHttpDataSource(
Call.Factory callFactory, Call.Factory callFactory,
@Nullable String userAgent, @Nullable String userAgent,
...@@ -110,9 +134,12 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -110,9 +134,12 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
* predicate then a {@link InvalidContentTypeException} is thrown from {@link * predicate then a {@link InvalidContentTypeException} is thrown from {@link
* #open(DataSpec)}. * #open(DataSpec)}.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to * @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
* the server as HTTP headers on every request. * server as HTTP headers on every request.
* @deprecated Use {@link #OkHttpDataSource(Call.Factory, String, CacheControl,
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
*/ */
@Deprecated
public OkHttpDataSource( public OkHttpDataSource(
Call.Factory callFactory, Call.Factory callFactory,
@Nullable String userAgent, @Nullable String userAgent,
...@@ -128,8 +155,20 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -128,8 +155,20 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
this.requestProperties = new RequestProperties(); this.requestProperties = new RequestProperties();
} }
/**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
*
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
* predicate that was previously set.
*/
public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
this.contentTypePredicate = contentTypePredicate;
}
@Override @Override
public @Nullable Uri getUri() { @Nullable
public Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString()); return response == null ? null : Uri.parse(response.request().url().toString());
} }
......
...@@ -29,9 +29,9 @@ import okhttp3.Call; ...@@ -29,9 +29,9 @@ import okhttp3.Call;
public final class OkHttpDataSourceFactory extends BaseFactory { public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory; private final Call.Factory callFactory;
private final @Nullable String userAgent; @Nullable private final String userAgent;
private final @Nullable TransferListener listener; @Nullable private final TransferListener listener;
private final @Nullable CacheControl cacheControl; @Nullable private final CacheControl cacheControl;
/** /**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
...@@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory { ...@@ -89,7 +89,6 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
new OkHttpDataSource( new OkHttpDataSource(
callFactory, callFactory,
userAgent, userAgent,
/* contentTypePredicate= */ null,
cacheControl, cacheControl,
defaultRequestProperties); defaultRequestProperties);
if (listener != null) { if (listener != null) {
......
/*
* 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.okhttp;
import com.google.android.exoplayer2.util.NonNullApi;
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