mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
eb458555a6
517 changed files with 15010 additions and 7120 deletions
18
.github/ISSUE_TEMPLATE/bug.md
vendored
18
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -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.
|
||||||
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".
|
||||||
|
|
|
||||||
19
.github/ISSUE_TEMPLATE/content_not_playing.md
vendored
19
.github/ISSUE_TEMPLATE/content_not_playing.md
vendored
|
|
@ -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.
|
||||||
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".
|
||||||
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
|
||||||
|
|
|
||||||
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -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:
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
|
||||||
25
.github/ISSUE_TEMPLATE/question.md
vendored
25
.github/ISSUE_TEMPLATE/question.md
vendored
|
|
@ -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.
|
||||||
### [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
|
||||||
|
|
|
||||||
129
RELEASENOTES.md
129
RELEASENOTES.md
|
|
@ -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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
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 @@
|
||||||
// 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'
|
||||||
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'
|
||||||
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
|
||||||
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
|
||||||
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 {
|
||||||
// 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 @@
|
||||||
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"
|
||||||
|
|
|
||||||
|
|
@ -1,405 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.exoplayer2.castdemo;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
|
||||||
import com.google.android.exoplayer2.Player.EventListener;
|
|
||||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.Timeline.Period;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
|
||||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
|
||||||
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.ui.PlayerControlView;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
|
||||||
import com.google.android.gms.cast.MediaMetadata;
|
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
|
|
||||||
/* package */ class DefaultReceiverPlayerManager
|
|
||||||
implements PlayerManager, EventListener, SessionAvailabilityListener {
|
|
||||||
|
|
||||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
|
||||||
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
|
||||||
new DefaultHttpDataSourceFactory(USER_AGENT);
|
|
||||||
|
|
||||||
private final PlayerView localPlayerView;
|
|
||||||
private final PlayerControlView castControlView;
|
|
||||||
private final SimpleExoPlayer exoPlayer;
|
|
||||||
private final CastPlayer castPlayer;
|
|
||||||
private final ArrayList<MediaItem> mediaQueue;
|
|
||||||
private final Listener listener;
|
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
|
||||||
|
|
||||||
private boolean castMediaQueueCreationPending;
|
|
||||||
private int currentItemIndex;
|
|
||||||
private Player currentPlayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
|
||||||
*
|
|
||||||
* @param listener A {@link Listener} for queue position changes.
|
|
||||||
* @param localPlayerView The {@link PlayerView} for local playback.
|
|
||||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
|
||||||
* @param context A {@link Context}.
|
|
||||||
* @param castContext The {@link CastContext}.
|
|
||||||
*/
|
|
||||||
public DefaultReceiverPlayerManager(
|
|
||||||
Listener listener,
|
|
||||||
PlayerView localPlayerView,
|
|
||||||
PlayerControlView castControlView,
|
|
||||||
Context context,
|
|
||||||
CastContext castContext) {
|
|
||||||
this.listener = listener;
|
|
||||||
this.localPlayerView = localPlayerView;
|
|
||||||
this.castControlView = castControlView;
|
|
||||||
mediaQueue = new ArrayList<>();
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
|
||||||
|
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
|
||||||
exoPlayer.addListener(this);
|
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
|
||||||
|
|
||||||
castPlayer = new CastPlayer(castContext);
|
|
||||||
castPlayer.addListener(this);
|
|
||||||
castPlayer.setSessionAvailabilityListener(this);
|
|
||||||
castControlView.setPlayer(castPlayer);
|
|
||||||
|
|
||||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue manipulation methods.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays a specified queue item in the current player.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void selectQueueItem(int itemIndex) {
|
|
||||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the index of the currently played item. */
|
|
||||||
@Override
|
|
||||||
public int getCurrentItemIndex() {
|
|
||||||
return currentItemIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends {@code item} to the media queue.
|
|
||||||
*
|
|
||||||
* @param item The {@link MediaItem} to append.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addItem(MediaItem item) {
|
|
||||||
mediaQueue.add(item);
|
|
||||||
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
castPlayer.addItems(buildMediaQueueItem(item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the size of the media queue. */
|
|
||||||
@Override
|
|
||||||
public int getMediaQueueSize() {
|
|
||||||
return mediaQueue.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the item at the given index in the media queue.
|
|
||||||
*
|
|
||||||
* @param position The index of the item.
|
|
||||||
* @return The item at the given index in the media queue.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public MediaItem getItem(int position) {
|
|
||||||
return mediaQueue.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the item at the given index from the media queue.
|
|
||||||
*
|
|
||||||
* @param item The item to remove.
|
|
||||||
* @return Whether the removal was successful.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean removeItem(MediaItem item) {
|
|
||||||
int itemIndex = mediaQueue.indexOf(item);
|
|
||||||
if (itemIndex == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
|
||||||
if (currentPlayer == castPlayer) {
|
|
||||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
|
||||||
if (castTimeline.getPeriodCount() <= itemIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaQueue.remove(itemIndex);
|
|
||||||
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
|
||||||
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
|
||||||
} else if (itemIndex < currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves an item within the queue.
|
|
||||||
*
|
|
||||||
* @param item The item to move.
|
|
||||||
* @param toIndex The target index of the item in the queue.
|
|
||||||
* @return Whether the item move was successful.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean moveItem(MediaItem item, int toIndex) {
|
|
||||||
int fromIndex = mediaQueue.indexOf(item);
|
|
||||||
if (fromIndex == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Player update.
|
|
||||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
|
||||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
|
||||||
int periodCount = castTimeline.getPeriodCount();
|
|
||||||
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
|
||||||
castPlayer.moveItem(elementId, toIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
|
||||||
|
|
||||||
// Index update.
|
|
||||||
if (fromIndex == currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(toIndex);
|
|
||||||
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
|
||||||
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
|
||||||
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
|
||||||
*
|
|
||||||
* @param event The {@link KeyEvent}.
|
|
||||||
* @return Whether the event was handled by the target view.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
return localPlayerView.dispatchKeyEvent(event);
|
|
||||||
} else /* currentPlayer == castPlayer */ {
|
|
||||||
return castControlView.dispatchKeyEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Releases the manager and the players that it holds. */
|
|
||||||
@Override
|
|
||||||
public void release() {
|
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
|
||||||
mediaQueue.clear();
|
|
||||||
concatenatingMediaSource.clear();
|
|
||||||
castPlayer.setSessionAvailabilityListener(null);
|
|
||||||
castPlayer.release();
|
|
||||||
localPlayerView.setPlayer(null);
|
|
||||||
exoPlayer.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player.EventListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimelineChanged(
|
|
||||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
|
||||||
if (currentPlayer == castPlayer && timeline.isEmpty()) {
|
|
||||||
castMediaQueueCreationPending = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CastPlayer.SessionAvailabilityListener implementation.
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionAvailable() {
|
|
||||||
setCurrentPlayer(castPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCastSessionUnavailable() {
|
|
||||||
setCurrentPlayer(exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods.
|
|
||||||
|
|
||||||
private void updateCurrentItemIndex() {
|
|
||||||
int playbackState = currentPlayer.getPlaybackState();
|
|
||||||
maybeSetCurrentItemAndNotify(
|
|
||||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
|
||||||
? currentPlayer.getCurrentWindowIndex()
|
|
||||||
: C.INDEX_UNSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCurrentPlayer(Player currentPlayer) {
|
|
||||||
if (this.currentPlayer == currentPlayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// View management.
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
localPlayerView.setVisibility(View.VISIBLE);
|
|
||||||
castControlView.hide();
|
|
||||||
} else /* currentPlayer == castPlayer */ {
|
|
||||||
localPlayerView.setVisibility(View.GONE);
|
|
||||||
castControlView.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player state management.
|
|
||||||
long playbackPositionMs = C.TIME_UNSET;
|
|
||||||
int windowIndex = C.INDEX_UNSET;
|
|
||||||
boolean playWhenReady = false;
|
|
||||||
if (this.currentPlayer != null) {
|
|
||||||
int playbackState = this.currentPlayer.getPlaybackState();
|
|
||||||
if (playbackState != Player.STATE_ENDED) {
|
|
||||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
|
||||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
|
||||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
|
||||||
if (windowIndex != currentItemIndex) {
|
|
||||||
playbackPositionMs = C.TIME_UNSET;
|
|
||||||
windowIndex = currentItemIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.currentPlayer.stop(true);
|
|
||||||
} else {
|
|
||||||
// This is the initial setup. No need to save any state.
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPlayer = currentPlayer;
|
|
||||||
|
|
||||||
// Media queue management.
|
|
||||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
|
||||||
if (currentPlayer == exoPlayer) {
|
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playback transition.
|
|
||||||
if (windowIndex != C.INDEX_UNSET) {
|
|
||||||
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts playback of the item at the given position.
|
|
||||||
*
|
|
||||||
* @param itemIndex The index of the item to play.
|
|
||||||
* @param positionMs The position at which playback should start.
|
|
||||||
* @param playWhenReady Whether the player should proceed when ready to do so.
|
|
||||||
*/
|
|
||||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
|
||||||
maybeSetCurrentItemAndNotify(itemIndex);
|
|
||||||
if (castMediaQueueCreationPending) {
|
|
||||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
|
||||||
for (int i = 0; i < items.length; i++) {
|
|
||||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
|
||||||
}
|
|
||||||
castMediaQueueCreationPending = false;
|
|
||||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
|
||||||
} else {
|
|
||||||
currentPlayer.seekTo(itemIndex, positionMs);
|
|
||||||
currentPlayer.setPlayWhenReady(playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
|
||||||
if (this.currentItemIndex != currentItemIndex) {
|
|
||||||
int oldIndex = this.currentItemIndex;
|
|
||||||
this.currentItemIndex = currentItemIndex;
|
|
||||||
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(MediaItem item) {
|
|
||||||
Uri uri = item.media.uri;
|
|
||||||
switch (item.mimeType) {
|
|
||||||
case DemoUtil.MIME_TYPE_SS:
|
|
||||||
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_DASH:
|
|
||||||
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_HLS:
|
|
||||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
|
||||||
return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
throw new IllegalStateException("Unsupported type: " + item.mimeType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MediaQueueItem buildMediaQueueItem(MediaItem item) {
|
|
||||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
|
||||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title);
|
|
||||||
MediaInfo mediaInfo =
|
|
||||||
new MediaInfo.Builder(item.media.uri.toString())
|
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
|
||||||
.setContentType(item.mimeType)
|
|
||||||
.setMetadata(movieMetadata)
|
|
||||||
.build();
|
|
||||||
return new MediaQueueItem.Builder(mediaInfo).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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(
|
samples.add(
|
||||||
new Sample(
|
new MediaItem.Builder()
|
||||||
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4));
|
.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(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(
|
||||||
|
Uri.parse(
|
||||||
|
"https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"))
|
||||||
|
.setTitle("Widevine DASH cbcs: Tears")
|
||||||
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
|
.setDrmConfiguration(
|
||||||
|
new DrmConfiguration(
|
||||||
|
C.WIDEVINE_UUID,
|
||||||
|
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||||
|
Collections.emptyMap()))
|
||||||
|
.build());
|
||||||
|
|
||||||
SAMPLES = Collections.unmodifiableList(samples);
|
SAMPLES = Collections.unmodifiableList(samples);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
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;
|
||||||
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
|
||||||
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
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,51 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.castdemo;
|
package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.View;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||||
|
import com.google.android.exoplayer2.Player.EventListener;
|
||||||
|
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.Timeline.Period;
|
||||||
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
|
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||||
|
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||||
|
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||||
|
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
||||||
|
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
|
||||||
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
|
||||||
|
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
||||||
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
|
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;
|
||||||
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||||
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
|
import com.google.android.gms.cast.MediaQueueItem;
|
||||||
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/** Manages the players in the Cast demo app. */
|
/** Manages players and an internal media queue for the demo app. */
|
||||||
/* package */ interface PlayerManager {
|
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||||
|
|
||||||
/** Listener for events. */
|
/** Listener for events. */
|
||||||
interface Listener {
|
interface Listener {
|
||||||
|
|
@ -28,40 +67,418 @@ import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
/** Called when the currently played item of the media queue changes. */
|
/** Called when the currently played item of the media queue changes. */
|
||||||
void onQueuePositionChanged(int previousIndex, int newIndex);
|
void onQueuePositionChanged(int previousIndex, int newIndex);
|
||||||
|
|
||||||
/** Called when the media queue changes due to modifications not caused by this manager. */
|
/**
|
||||||
void onQueueContentsExternallyChanged();
|
* Called when a track of type {@code trackType} is not supported by the player.
|
||||||
|
*
|
||||||
/** Called when an error occurs in the current player. */
|
* @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants.
|
||||||
void onPlayerError();
|
*/
|
||||||
|
void onUnsupportedTrack(int trackType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Redirects the given {@code keyEvent} to the active player. */
|
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||||
boolean dispatchKeyEvent(KeyEvent keyEvent);
|
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
|
||||||
|
new DefaultHttpDataSourceFactory(USER_AGENT);
|
||||||
|
|
||||||
/** Appends the given {@link MediaItem} to the media queue. */
|
private final PlayerView localPlayerView;
|
||||||
void addItem(MediaItem mediaItem);
|
private final PlayerControlView castControlView;
|
||||||
|
private final DefaultTrackSelector trackSelector;
|
||||||
|
private final SimpleExoPlayer exoPlayer;
|
||||||
|
private final CastPlayer castPlayer;
|
||||||
|
private final ArrayList<MediaItem> mediaQueue;
|
||||||
|
private final Listener listener;
|
||||||
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
private final MediaItemConverter mediaItemConverter;
|
||||||
|
private final IdentityHashMap<MediaSource, FrameworkMediaDrm> mediaDrms;
|
||||||
|
|
||||||
/** Returns the number of items in the media queue. */
|
private TrackGroupArray lastSeenTrackGroupArray;
|
||||||
int getMediaQueueSize();
|
private int currentItemIndex;
|
||||||
|
private Player currentPlayer;
|
||||||
/** Selects the item at the given position for playback. */
|
|
||||||
void selectQueueItem(int position);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is
|
* Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||||
* being played.
|
*
|
||||||
|
* @param listener A {@link Listener} for queue position changes.
|
||||||
|
* @param localPlayerView The {@link PlayerView} for local playback.
|
||||||
|
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param castContext The {@link CastContext}.
|
||||||
*/
|
*/
|
||||||
int getCurrentItemIndex();
|
public PlayerManager(
|
||||||
|
Listener listener,
|
||||||
|
PlayerView localPlayerView,
|
||||||
|
PlayerControlView castControlView,
|
||||||
|
Context context,
|
||||||
|
CastContext castContext) {
|
||||||
|
this.listener = listener;
|
||||||
|
this.localPlayerView = localPlayerView;
|
||||||
|
this.castControlView = castControlView;
|
||||||
|
mediaQueue = new ArrayList<>();
|
||||||
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
|
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||||
|
mediaItemConverter = new DefaultMediaItemConverter();
|
||||||
|
mediaDrms = new IdentityHashMap<>();
|
||||||
|
|
||||||
/** Returns the {@link MediaItem} at the given {@code position}. */
|
trackSelector = new DefaultTrackSelector(context);
|
||||||
MediaItem getItem(int position);
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
||||||
|
exoPlayer.addListener(this);
|
||||||
|
localPlayerView.setPlayer(exoPlayer);
|
||||||
|
|
||||||
/** Moves the item at position {@code from} to position {@code to}. */
|
castPlayer = new CastPlayer(castContext);
|
||||||
boolean moveItem(MediaItem item, int to);
|
castPlayer.addListener(this);
|
||||||
|
castPlayer.setSessionAvailabilityListener(this);
|
||||||
|
castControlView.setPlayer(castPlayer);
|
||||||
|
|
||||||
/** Removes the item at position {@code index}. */
|
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||||
boolean removeItem(MediaItem item);
|
}
|
||||||
|
|
||||||
/** Releases any acquired resources. */
|
// Queue manipulation methods.
|
||||||
void release();
|
|
||||||
|
/**
|
||||||
|
* Plays a specified queue item in the current player.
|
||||||
|
*
|
||||||
|
* @param itemIndex The index of the item to play.
|
||||||
|
*/
|
||||||
|
public void selectQueueItem(int itemIndex) {
|
||||||
|
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the index of the currently played item. */
|
||||||
|
public int getCurrentItemIndex() {
|
||||||
|
return currentItemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends {@code item} to the media queue.
|
||||||
|
*
|
||||||
|
* @param item The {@link MediaItem} to append.
|
||||||
|
*/
|
||||||
|
public void addItem(MediaItem item) {
|
||||||
|
mediaQueue.add(item);
|
||||||
|
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||||
|
if (currentPlayer == castPlayer) {
|
||||||
|
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the size of the media queue. */
|
||||||
|
public int getMediaQueueSize() {
|
||||||
|
return mediaQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the item at the given index in the media queue.
|
||||||
|
*
|
||||||
|
* @param position The index of the item.
|
||||||
|
* @return The item at the given index in the media queue.
|
||||||
|
*/
|
||||||
|
public MediaItem getItem(int position) {
|
||||||
|
return mediaQueue.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the item at the given index from the media queue.
|
||||||
|
*
|
||||||
|
* @param item The item to remove.
|
||||||
|
* @return Whether the removal was successful.
|
||||||
|
*/
|
||||||
|
public boolean removeItem(MediaItem item) {
|
||||||
|
int itemIndex = mediaQueue.indexOf(item);
|
||||||
|
if (itemIndex == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||||
|
releaseMediaDrmOfMediaSource(removedMediaSource);
|
||||||
|
if (currentPlayer == castPlayer) {
|
||||||
|
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
|
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||||
|
if (castTimeline.getPeriodCount() <= itemIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaQueue.remove(itemIndex);
|
||||||
|
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
|
||||||
|
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
|
||||||
|
} else if (itemIndex < currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves an item within the queue.
|
||||||
|
*
|
||||||
|
* @param item The item to move.
|
||||||
|
* @param toIndex The target index of the item in the queue.
|
||||||
|
* @return Whether the item move was successful.
|
||||||
|
*/
|
||||||
|
public boolean moveItem(MediaItem item, int toIndex) {
|
||||||
|
int fromIndex = mediaQueue.indexOf(item);
|
||||||
|
if (fromIndex == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Player update.
|
||||||
|
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||||
|
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
|
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||||
|
int periodCount = castTimeline.getPeriodCount();
|
||||||
|
if (periodCount <= fromIndex || periodCount <= toIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
|
||||||
|
castPlayer.moveItem(elementId, toIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
|
||||||
|
|
||||||
|
// Index update.
|
||||||
|
if (fromIndex == currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(toIndex);
|
||||||
|
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
|
||||||
|
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
|
||||||
|
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
||||||
|
*
|
||||||
|
* @param event The {@link KeyEvent}.
|
||||||
|
* @return Whether the event was handled by the target view.
|
||||||
|
*/
|
||||||
|
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||||
|
if (currentPlayer == exoPlayer) {
|
||||||
|
return localPlayerView.dispatchKeyEvent(event);
|
||||||
|
} else /* currentPlayer == castPlayer */ {
|
||||||
|
return castControlView.dispatchKeyEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Releases the manager and the players that it holds. */
|
||||||
|
public void release() {
|
||||||
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
|
mediaQueue.clear();
|
||||||
|
concatenatingMediaSource.clear();
|
||||||
|
for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) {
|
||||||
|
mediaDrm.release();
|
||||||
|
}
|
||||||
|
castPlayer.setSessionAvailabilityListener(null);
|
||||||
|
castPlayer.release();
|
||||||
|
localPlayerView.setPlayer(null);
|
||||||
|
exoPlayer.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player.EventListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||||
|
updateCurrentItemIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||||
|
updateCurrentItemIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||||
|
updateCurrentItemIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||||
|
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
|
||||||
|
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
|
||||||
|
trackSelector.getCurrentMappedTrackInfo();
|
||||||
|
if (mappedTrackInfo != null) {
|
||||||
|
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||||
|
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||||
|
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
|
||||||
|
}
|
||||||
|
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
|
||||||
|
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||||
|
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastSeenTrackGroupArray = trackGroups;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CastPlayer.SessionAvailabilityListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCastSessionAvailable() {
|
||||||
|
setCurrentPlayer(castPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCastSessionUnavailable() {
|
||||||
|
setCurrentPlayer(exoPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void updateCurrentItemIndex() {
|
||||||
|
int playbackState = currentPlayer.getPlaybackState();
|
||||||
|
maybeSetCurrentItemAndNotify(
|
||||||
|
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||||
|
? currentPlayer.getCurrentWindowIndex()
|
||||||
|
: C.INDEX_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCurrentPlayer(Player currentPlayer) {
|
||||||
|
if (this.currentPlayer == currentPlayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// View management.
|
||||||
|
if (currentPlayer == exoPlayer) {
|
||||||
|
localPlayerView.setVisibility(View.VISIBLE);
|
||||||
|
castControlView.hide();
|
||||||
|
} else /* currentPlayer == castPlayer */ {
|
||||||
|
localPlayerView.setVisibility(View.GONE);
|
||||||
|
castControlView.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player state management.
|
||||||
|
long playbackPositionMs = C.TIME_UNSET;
|
||||||
|
int windowIndex = C.INDEX_UNSET;
|
||||||
|
boolean playWhenReady = false;
|
||||||
|
|
||||||
|
Player previousPlayer = this.currentPlayer;
|
||||||
|
if (previousPlayer != null) {
|
||||||
|
// Save state from the previous player.
|
||||||
|
int playbackState = previousPlayer.getPlaybackState();
|
||||||
|
if (playbackState != Player.STATE_ENDED) {
|
||||||
|
playbackPositionMs = previousPlayer.getCurrentPosition();
|
||||||
|
playWhenReady = previousPlayer.getPlayWhenReady();
|
||||||
|
windowIndex = previousPlayer.getCurrentWindowIndex();
|
||||||
|
if (windowIndex != currentItemIndex) {
|
||||||
|
playbackPositionMs = C.TIME_UNSET;
|
||||||
|
windowIndex = currentItemIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousPlayer.stop(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentPlayer = currentPlayer;
|
||||||
|
|
||||||
|
// Media queue management.
|
||||||
|
if (currentPlayer == exoPlayer) {
|
||||||
|
exoPlayer.prepare(concatenatingMediaSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playback transition.
|
||||||
|
if (windowIndex != C.INDEX_UNSET) {
|
||||||
|
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts playback of the item at the given position.
|
||||||
|
*
|
||||||
|
* @param itemIndex The index of the item to play.
|
||||||
|
* @param positionMs The position at which playback should start.
|
||||||
|
* @param playWhenReady Whether the player should proceed when ready to do so.
|
||||||
|
*/
|
||||||
|
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||||
|
maybeSetCurrentItemAndNotify(itemIndex);
|
||||||
|
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||||
|
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||||
|
for (int i = 0; i < items.length; i++) {
|
||||||
|
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
|
||||||
|
}
|
||||||
|
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||||
|
} else {
|
||||||
|
currentPlayer.seekTo(itemIndex, positionMs);
|
||||||
|
currentPlayer.setPlayWhenReady(playWhenReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
|
||||||
|
if (this.currentItemIndex != currentItemIndex) {
|
||||||
|
int oldIndex = this.currentItemIndex;
|
||||||
|
this.currentItemIndex = currentItemIndex;
|
||||||
|
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaSource buildMediaSource(MediaItem item) {
|
||||||
|
Uri uri = item.uri;
|
||||||
|
String mimeType = item.mimeType;
|
||||||
|
if (mimeType == null) {
|
||||||
|
throw new IllegalArgumentException("mimeType is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameworkMediaDrm mediaDrm = null;
|
||||||
|
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager =
|
||||||
|
DrmSessionManager.getDummyDrmSessionManager();
|
||||||
|
MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
|
||||||
|
if (drmConfiguration != null) {
|
||||||
|
String licenseServerUrl =
|
||||||
|
drmConfiguration.licenseUri != null ? drmConfiguration.licenseUri.toString() : "";
|
||||||
|
HttpMediaDrmCallback drmCallback =
|
||||||
|
new HttpMediaDrmCallback(licenseServerUrl, DATA_SOURCE_FACTORY);
|
||||||
|
for (Map.Entry<String, String> requestHeader : drmConfiguration.requestHeaders.entrySet()) {
|
||||||
|
drmCallback.setKeyRequestProperty(requestHeader.getKey(), requestHeader.getValue());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mediaDrm = FrameworkMediaDrm.newInstance(drmConfiguration.uuid);
|
||||||
|
drmSessionManager =
|
||||||
|
new DefaultDrmSessionManager<>(
|
||||||
|
drmConfiguration.uuid,
|
||||||
|
mediaDrm,
|
||||||
|
drmCallback,
|
||||||
|
/* optionalKeyRequestParameters= */ null,
|
||||||
|
/* multiSession= */ true);
|
||||||
|
} catch (UnsupportedDrmException e) {
|
||||||
|
// Do nothing. The track selector will avoid selecting the DRM protected tracks.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaSource createdMediaSource;
|
||||||
|
switch (mimeType) {
|
||||||
|
case DemoUtil.MIME_TYPE_SS:
|
||||||
|
createdMediaSource =
|
||||||
|
new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
|
case DemoUtil.MIME_TYPE_DASH:
|
||||||
|
createdMediaSource =
|
||||||
|
new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
|
case DemoUtil.MIME_TYPE_HLS:
|
||||||
|
createdMediaSource =
|
||||||
|
new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
|
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||||
|
createdMediaSource =
|
||||||
|
new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
|
||||||
|
}
|
||||||
|
if (mediaDrm != null) {
|
||||||
|
mediaDrms.put(createdMediaSource, mediaDrm);
|
||||||
|
}
|
||||||
|
return createdMediaSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) {
|
||||||
|
FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource);
|
||||||
|
if (mediaDrmToRelease != null) {
|
||||||
|
mediaDrmToRelease.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
4
demos/gvr/README.md
Normal file
4
demos/gvr/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# ExoPlayer VR player demo #
|
||||||
|
|
||||||
|
This folder contains a demo application that showcases 360 video playback using
|
||||||
|
ExoPlayer GVR extension.
|
||||||
59
demos/gvr/build.gradle
Normal file
59
demos/gvr/build.gradle
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// 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.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
versionName project.ext.releaseVersion
|
||||||
|
versionCode project.ext.releaseVersionCode
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
shrinkResources true
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
jniDebuggable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
// The demo app isn't indexed and doesn't have translations.
|
||||||
|
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(modulePrefix + 'library-core')
|
||||||
|
implementation project(modulePrefix + 'library-ui')
|
||||||
|
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'
|
||||||
74
demos/gvr/src/main/AndroidManifest.xml
Normal file
74
demos/gvr/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- Copyright (C) 2018 The Android Open Source Project
|
<!-- Copyright (C) 2016 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,13 @@
|
||||||
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.
|
||||||
-->
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<manifest package="com.google.android.exoplayer2.testutil"/>
|
<ListView android:id="@+id/sample_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
BIN
demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
28
demos/gvr/src/main/res/values/strings.xml
Normal file
28
demos/gvr/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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.
|
||||||
|
-->
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
|
||||||
|
<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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
@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 {
|
||||||
downloadHelper.addTrackSelectionForSingleRenderer(
|
downloadHelper.addTrackSelectionForSingleRenderer(
|
||||||
periodIndex,
|
periodIndex,
|
||||||
/* rendererIndex= */ i,
|
/* rendererIndex= */ i,
|
||||||
DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS,
|
trackSelectorParameters,
|
||||||
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
|
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@ import com.google.android.exoplayer2.PlaybackPreparer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.demo.Sample.UriSample;
|
||||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||||
|
|
@ -77,41 +79,48 @@ import java.lang.reflect.Constructor;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||||
public class PlayerActivity extends AppCompatActivity
|
public class PlayerActivity extends AppCompatActivity
|
||||||
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
|
||||||
|
|
||||||
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
// Activity extras.
|
||||||
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
|
||||||
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
|
||||||
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
|
||||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
|
||||||
|
|
||||||
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
|
||||||
public static final String EXTENSION_EXTRA = "extension";
|
|
||||||
|
|
||||||
public static final String ACTION_VIEW_LIST =
|
|
||||||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
|
||||||
public static final String URI_LIST_EXTRA = "uri_list";
|
|
||||||
public static final String EXTENSION_LIST_EXTRA = "extension_list";
|
|
||||||
|
|
||||||
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
|
||||||
|
|
||||||
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
|
||||||
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
|
||||||
public static final String ABR_ALGORITHM_RANDOM = "random";
|
|
||||||
|
|
||||||
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
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_MONO = "mono";
|
||||||
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||||
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||||
|
|
||||||
|
// Actions.
|
||||||
|
|
||||||
|
public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
|
||||||
|
public static final String ACTION_VIEW_LIST =
|
||||||
|
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||||
|
|
||||||
|
// Player configuration extras.
|
||||||
|
|
||||||
|
public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
|
||||||
|
public static final String ABR_ALGORITHM_DEFAULT = "default";
|
||||||
|
public static final String ABR_ALGORITHM_RANDOM = "random";
|
||||||
|
|
||||||
|
// Media item configuration extras.
|
||||||
|
|
||||||
|
public static final String URI_EXTRA = "uri";
|
||||||
|
public static final String EXTENSION_EXTRA = "extension";
|
||||||
|
|
||||||
|
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
|
||||||
|
public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
|
||||||
|
public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
|
||||||
|
public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
|
||||||
|
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||||
|
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
|
||||||
// For backwards compatibility only.
|
// For backwards compatibility only.
|
||||||
private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
|
||||||
|
|
||||||
// Saved instance state keys.
|
// Saved instance state keys.
|
||||||
|
|
||||||
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
|
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
|
||||||
private static final String KEY_WINDOW = "window";
|
private static final String KEY_WINDOW = "window";
|
||||||
private static final String KEY_POSITION = "position";
|
private static final String KEY_POSITION = "position";
|
||||||
|
|
@ -123,6 +132,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final ArrayList<FrameworkMediaDrm> mediaDrms;
|
||||||
|
|
||||||
private PlayerView playerView;
|
private PlayerView playerView;
|
||||||
private LinearLayout debugRootView;
|
private LinearLayout debugRootView;
|
||||||
private Button selectTracksButton;
|
private Button selectTracksButton;
|
||||||
|
|
@ -131,7 +142,6 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
private DataSource.Factory dataSourceFactory;
|
private DataSource.Factory dataSourceFactory;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
private FrameworkMediaDrm mediaDrm;
|
|
||||||
private MediaSource mediaSource;
|
private MediaSource mediaSource;
|
||||||
private DefaultTrackSelector trackSelector;
|
private DefaultTrackSelector trackSelector;
|
||||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||||
|
|
@ -147,11 +157,16 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
private AdsLoader adsLoader;
|
private AdsLoader adsLoader;
|
||||||
private Uri loadedAdTagUri;
|
private Uri loadedAdTagUri;
|
||||||
|
|
||||||
|
public PlayerActivity() {
|
||||||
|
mediaDrms = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
// Activity lifecycle
|
// Activity lifecycle
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
Intent intent = getIntent();
|
||||||
|
String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||||
if (sphericalStereoMode != null) {
|
if (sphericalStereoMode != null) {
|
||||||
setTheme(R.style.PlayerTheme_Spherical);
|
setTheme(R.style.PlayerTheme_Spherical);
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +208,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
startWindow = savedInstanceState.getInt(KEY_WINDOW);
|
startWindow = savedInstanceState.getInt(KEY_WINDOW);
|
||||||
startPosition = savedInstanceState.getLong(KEY_POSITION);
|
startPosition = savedInstanceState.getLong(KEY_POSITION);
|
||||||
} else {
|
} else {
|
||||||
trackSelectorParameters = new DefaultTrackSelector.ParametersBuilder().build();
|
trackSelectorParameters = DefaultTrackSelector.Parameters.getDefaults(/* context= */ this);
|
||||||
clearStartPosition();
|
clearStartPosition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -327,67 +342,11 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
private void initializePlayer() {
|
private void initializePlayer() {
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
String action = intent.getAction();
|
|
||||||
Uri[] uris;
|
|
||||||
String[] extensions;
|
|
||||||
if (ACTION_VIEW.equals(action)) {
|
|
||||||
uris = new Uri[] {intent.getData()};
|
|
||||||
extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)};
|
|
||||||
} else if (ACTION_VIEW_LIST.equals(action)) {
|
|
||||||
String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA);
|
|
||||||
uris = new Uri[uriStrings.length];
|
|
||||||
for (int i = 0; i < uriStrings.length; i++) {
|
|
||||||
uris[i] = Uri.parse(uriStrings[i]);
|
|
||||||
}
|
|
||||||
extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA);
|
|
||||||
if (extensions == null) {
|
|
||||||
extensions = new String[uriStrings.length];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast(getString(R.string.unexpected_intent_action, action));
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Util.checkCleartextTrafficPermitted(uris)) {
|
|
||||||
showToast(R.string.error_cleartext_not_permitted);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, uris)) {
|
|
||||||
// The player will be reinitialized if the permission is granted.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
releaseMediaDrms();
|
||||||
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
|
mediaSource = createTopLevelMediaSource(intent);
|
||||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);
|
if (mediaSource == null) {
|
||||||
String[] keyRequestPropertiesArray =
|
return;
|
||||||
intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA);
|
|
||||||
boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA, false);
|
|
||||||
int errorStringId = R.string.error_drm_unknown;
|
|
||||||
if (Util.SDK_INT < 18) {
|
|
||||||
errorStringId = R.string.error_drm_not_supported;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA
|
|
||||||
: DRM_SCHEME_UUID_EXTRA;
|
|
||||||
UUID drmSchemeUuid = Util.getDrmUuid(intent.getStringExtra(drmSchemeExtra));
|
|
||||||
if (drmSchemeUuid == null) {
|
|
||||||
errorStringId = R.string.error_drm_unsupported_scheme;
|
|
||||||
} else {
|
|
||||||
drmSessionManager =
|
|
||||||
buildDrmSessionManagerV18(
|
|
||||||
drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession);
|
|
||||||
}
|
|
||||||
} catch (UnsupportedDrmException e) {
|
|
||||||
errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
|
||||||
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (drmSessionManager == null) {
|
|
||||||
showToast(errorStringId);
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSelection.Factory trackSelectionFactory;
|
TrackSelection.Factory trackSelectionFactory;
|
||||||
|
|
@ -407,13 +366,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
RenderersFactory renderersFactory =
|
RenderersFactory renderersFactory =
|
||||||
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||||
|
|
||||||
trackSelector = new DefaultTrackSelector(trackSelectionFactory);
|
trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
|
||||||
trackSelector.setParameters(trackSelectorParameters);
|
trackSelector.setParameters(trackSelectorParameters);
|
||||||
lastSeenTrackGroupArray = null;
|
lastSeenTrackGroupArray = null;
|
||||||
|
|
||||||
player =
|
player =
|
||||||
ExoPlayerFactory.newSimpleInstance(
|
ExoPlayerFactory.newSimpleInstance(/* context= */ this, renderersFactory, trackSelector);
|
||||||
/* context= */ this, renderersFactory, trackSelector, drmSessionManager);
|
|
||||||
player.addListener(new PlayerEventListener());
|
player.addListener(new PlayerEventListener());
|
||||||
player.setPlayWhenReady(startAutoPlay);
|
player.setPlayWhenReady(startAutoPlay);
|
||||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||||
|
|
@ -421,28 +379,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
playerView.setPlaybackPreparer(this);
|
playerView.setPlaybackPreparer(this);
|
||||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||||
debugViewHelper.start();
|
debugViewHelper.start();
|
||||||
|
if (adsLoader != null) {
|
||||||
MediaSource[] mediaSources = new MediaSource[uris.length];
|
adsLoader.setPlayer(player);
|
||||||
for (int i = 0; i < uris.length; i++) {
|
|
||||||
mediaSources[i] = buildMediaSource(uris[i], extensions[i]);
|
|
||||||
}
|
|
||||||
mediaSource =
|
|
||||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
|
||||||
String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA);
|
|
||||||
if (adTagUriString != null) {
|
|
||||||
Uri adTagUri = Uri.parse(adTagUriString);
|
|
||||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
|
||||||
releaseAdsLoader();
|
|
||||||
loadedAdTagUri = adTagUri;
|
|
||||||
}
|
|
||||||
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString));
|
|
||||||
if (adsMediaSource != null) {
|
|
||||||
mediaSource = adsMediaSource;
|
|
||||||
} else {
|
|
||||||
showToast(R.string.ima_not_loaded);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
releaseAdsLoader();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||||
|
|
@ -453,26 +391,130 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
updateButtonVisibility();
|
updateButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSource buildMediaSource(Uri uri) {
|
@Nullable
|
||||||
return buildMediaSource(uri, null);
|
private MediaSource createTopLevelMediaSource(Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
|
||||||
|
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
|
||||||
|
showToast(getString(R.string.unexpected_intent_action, action));
|
||||||
|
finish();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sample intentAsSample = Sample.createFromIntent(intent);
|
||||||
|
UriSample[] samples =
|
||||||
|
intentAsSample instanceof Sample.PlaylistSample
|
||||||
|
? ((Sample.PlaylistSample) intentAsSample).children
|
||||||
|
: new UriSample[] {(UriSample) intentAsSample};
|
||||||
|
|
||||||
|
boolean seenAdsTagUri = false;
|
||||||
|
for (UriSample sample : samples) {
|
||||||
|
seenAdsTagUri |= sample.adTagUri != null;
|
||||||
|
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
|
||||||
|
showToast(R.string.error_cleartext_not_permitted);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
|
||||||
|
// The player will be reinitialized if the permission is granted.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaSource[] mediaSources = new MediaSource[samples.length];
|
||||||
|
for (int i = 0; i < samples.length; i++) {
|
||||||
|
mediaSources[i] = createLeafMediaSource(samples[i]);
|
||||||
|
}
|
||||||
|
MediaSource mediaSource =
|
||||||
|
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
||||||
|
|
||||||
|
if (seenAdsTagUri) {
|
||||||
|
Uri adTagUri = samples[0].adTagUri;
|
||||||
|
if (actionIsListView) {
|
||||||
|
showToast(R.string.unsupported_ads_in_concatenation);
|
||||||
|
} else {
|
||||||
|
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||||
|
releaseAdsLoader();
|
||||||
|
loadedAdTagUri = adTagUri;
|
||||||
|
}
|
||||||
|
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
|
||||||
|
if (adsMediaSource != null) {
|
||||||
|
mediaSource = adsMediaSource;
|
||||||
|
} else {
|
||||||
|
showToast(R.string.ima_not_loaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
releaseAdsLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
private MediaSource createLeafMediaSource(UriSample parameters) {
|
||||||
|
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||||
|
Sample.DrmInfo drmInfo = parameters.drmInfo;
|
||||||
|
if (drmInfo != null) {
|
||||||
|
int errorStringId = R.string.error_drm_unknown;
|
||||||
|
if (Util.SDK_INT < 18) {
|
||||||
|
errorStringId = R.string.error_drm_not_supported;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (drmInfo.drmScheme == null) {
|
||||||
|
errorStringId = R.string.error_drm_unsupported_scheme;
|
||||||
|
} else {
|
||||||
|
drmSessionManager =
|
||||||
|
buildDrmSessionManagerV18(
|
||||||
|
drmInfo.drmScheme,
|
||||||
|
drmInfo.drmLicenseUrl,
|
||||||
|
drmInfo.drmKeyRequestProperties,
|
||||||
|
drmInfo.drmMultiSession);
|
||||||
|
}
|
||||||
|
} catch (UnsupportedDrmException e) {
|
||||||
|
errorStringId =
|
||||||
|
e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||||
|
? R.string.error_drm_unsupported_scheme
|
||||||
|
: R.string.error_drm_unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (drmSessionManager == null) {
|
||||||
|
showToast(errorStringId);
|
||||||
|
finish();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
|
||||||
|
}
|
||||||
|
|
||||||
DownloadRequest downloadRequest =
|
DownloadRequest downloadRequest =
|
||||||
((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri);
|
((DemoApplication) getApplication())
|
||||||
|
.getDownloadTracker()
|
||||||
|
.getDownloadRequest(parameters.uri);
|
||||||
if (downloadRequest != null) {
|
if (downloadRequest != null) {
|
||||||
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
||||||
}
|
}
|
||||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
return createLeafMediaSource(parameters.uri, parameters.extension, drmSessionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaSource createLeafMediaSource(
|
||||||
|
Uri uri, String extension, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
|
||||||
|
@ContentType int type = Util.inferContentType(uri, extension);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case C.TYPE_DASH:
|
case C.TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new DashMediaSource.Factory(dataSourceFactory)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new SsMediaSource.Factory(dataSourceFactory)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
|
@ -491,8 +533,9 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
keyRequestPropertiesArray[i + 1]);
|
keyRequestPropertiesArray[i + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
releaseMediaDrm();
|
|
||||||
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
|
FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid);
|
||||||
|
mediaDrms.add(mediaDrm);
|
||||||
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
|
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -510,14 +553,14 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
if (adsLoader != null) {
|
if (adsLoader != null) {
|
||||||
adsLoader.setPlayer(null);
|
adsLoader.setPlayer(null);
|
||||||
}
|
}
|
||||||
releaseMediaDrm();
|
releaseMediaDrms();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseMediaDrm() {
|
private void releaseMediaDrms() {
|
||||||
if (mediaDrm != null) {
|
for (FrameworkMediaDrm mediaDrm : mediaDrms) {
|
||||||
mediaDrm.release();
|
mediaDrm.release();
|
||||||
mediaDrm = null;
|
|
||||||
}
|
}
|
||||||
|
mediaDrms.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseAdsLoader() {
|
private void releaseAdsLoader() {
|
||||||
|
|
@ -555,7 +598,8 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns an ads media source, reusing the ads loader if one exists. */
|
/** Returns an ads media source, reusing the ads loader if one exists. */
|
||||||
private @Nullable MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
@Nullable
|
||||||
|
private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
|
||||||
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
// Load the extension source using reflection so the demo app doesn't have to depend on it.
|
||||||
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||||
try {
|
try {
|
||||||
|
|
@ -570,12 +614,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
||||||
}
|
}
|
||||||
adsLoader.setPlayer(player);
|
|
||||||
MediaSourceFactory adMediaSourceFactory =
|
MediaSourceFactory adMediaSourceFactory =
|
||||||
new MediaSourceFactory() {
|
new MediaSourceFactory() {
|
||||||
@Override
|
@Override
|
||||||
public MediaSource createMediaSource(Uri uri) {
|
public MediaSource createMediaSource(Uri uri) {
|
||||||
return PlayerActivity.this.buildMediaSource(uri);
|
return PlayerActivity.this.createLeafMediaSource(
|
||||||
|
uri, /* extension=*/ null, DrmSessionManager.getDummyDrmSessionManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -678,7 +722,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
// Special case for decoder initialization failures.
|
// Special case for decoder initialization failures.
|
||||||
DecoderInitializationException decoderInitializationException =
|
DecoderInitializationException decoderInitializationException =
|
||||||
(DecoderInitializationException) cause;
|
(DecoderInitializationException) cause;
|
||||||
if (decoderInitializationException.decoderName == null) {
|
if (decoderInitializationException.codecInfo == null) {
|
||||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||||
errorString = getString(R.string.error_querying_decoders);
|
errorString = getString(R.string.error_querying_decoders);
|
||||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||||
|
|
@ -693,12 +737,11 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
errorString =
|
errorString =
|
||||||
getString(
|
getString(
|
||||||
R.string.error_instantiating_decoder,
|
R.string.error_instantiating_decoder,
|
||||||
decoderInitializationException.decoderName);
|
decoderInitializationException.codecInfo.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Pair.create(0, errorString);
|
return Pair.create(0, errorString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 @@
|
||||||
|
|
||||||
<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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
|
||||||
import com.google.android.gms.common.api.PendingResult;
|
import com.google.android.gms.common.api.PendingResult;
|
||||||
import com.google.android.gms.common.api.ResultCallback;
|
import com.google.android.gms.common.api.ResultCallback;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link Player} implementation that communicates with a Cast receiver app.
|
* {@link Player} implementation that communicates with a Cast receiver app.
|
||||||
|
|
@ -80,17 +83,18 @@ public final class CastPlayer extends BasePlayer {
|
||||||
private final CastTimelineTracker timelineTracker;
|
private final CastTimelineTracker timelineTracker;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
|
|
||||||
private RemoteMediaClient remoteMediaClient;
|
|
||||||
|
|
||||||
// Result callbacks.
|
// Result callbacks.
|
||||||
private final StatusListener statusListener;
|
private final StatusListener statusListener;
|
||||||
private final SeekResultCallback seekResultCallback;
|
private final SeekResultCallback seekResultCallback;
|
||||||
|
|
||||||
// Listeners.
|
// Listeners and notification.
|
||||||
private final CopyOnWriteArraySet<EventListener> listeners;
|
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||||
private SessionAvailabilityListener sessionAvailabilityListener;
|
private final ArrayList<ListenerNotificationTask> notificationsBatch;
|
||||||
|
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
|
||||||
|
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
|
||||||
|
|
||||||
// Internal state.
|
// Internal state.
|
||||||
|
@Nullable private RemoteMediaClient remoteMediaClient;
|
||||||
private CastTimeline currentTimeline;
|
private CastTimeline currentTimeline;
|
||||||
private TrackGroupArray currentTrackGroups;
|
private TrackGroupArray currentTrackGroups;
|
||||||
private TrackSelectionArray currentTrackSelection;
|
private TrackSelectionArray currentTrackSelection;
|
||||||
|
|
@ -113,7 +117,9 @@ public final class CastPlayer extends BasePlayer {
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
statusListener = new StatusListener();
|
statusListener = new StatusListener();
|
||||||
seekResultCallback = new SeekResultCallback();
|
seekResultCallback = new SeekResultCallback();
|
||||||
listeners = new CopyOnWriteArraySet<>();
|
listeners = new CopyOnWriteArrayList<>();
|
||||||
|
notificationsBatch = new ArrayList<>();
|
||||||
|
ongoingNotificationsTasks = new ArrayDeque<>();
|
||||||
|
|
||||||
SessionManager sessionManager = castContext.getSessionManager();
|
SessionManager sessionManager = castContext.getSessionManager();
|
||||||
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
|
||||||
|
|
@ -141,6 +147,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
* starts at position 0.
|
* starts at position 0.
|
||||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
|
||||||
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
|
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
|
||||||
}
|
}
|
||||||
|
|
@ -156,8 +163,9 @@ public final class CastPlayer extends BasePlayer {
|
||||||
* @param repeatMode The repeat mode for the created media queue.
|
* @param repeatMode The repeat mode for the created media queue.
|
||||||
* @return The Cast {@code PendingResult}, or null if no session is available.
|
* @return The Cast {@code PendingResult}, or null if no session is available.
|
||||||
*/
|
*/
|
||||||
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex,
|
@Nullable
|
||||||
long positionMs, @RepeatMode int repeatMode) {
|
public PendingResult<MediaChannelResult> loadItems(
|
||||||
|
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
|
||||||
if (remoteMediaClient != null) {
|
if (remoteMediaClient != null) {
|
||||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||||
waitingForInitialTimeline = true;
|
waitingForInitialTimeline = true;
|
||||||
|
|
@ -173,6 +181,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
* @param items The items to append.
|
* @param items The items to append.
|
||||||
* @return The Cast {@code PendingResult}, or null if no media queue exists.
|
* @return The Cast {@code PendingResult}, or null if no media queue exists.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
|
||||||
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
|
return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
|
||||||
}
|
}
|
||||||
|
|
@ -187,6 +196,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||||
* periodId} exist.
|
* periodId} exist.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
|
||||||
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|
||||||
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
|
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
|
||||||
|
|
@ -204,6 +214,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||||
* periodId} exist.
|
* periodId} exist.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
public PendingResult<MediaChannelResult> removeItem(int periodId) {
|
||||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||||
return remoteMediaClient.queueRemoveItem(periodId, null);
|
return remoteMediaClient.queueRemoveItem(periodId, null);
|
||||||
|
|
@ -222,6 +233,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
|
||||||
* periodId} exist.
|
* periodId} exist.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
|
||||||
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
|
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
|
||||||
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
|
||||||
|
|
@ -239,6 +251,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
* @return The item that corresponds to the period with the given id, or null if no media queue or
|
* @return The item that corresponds to the period with the given id, or null if no media queue or
|
||||||
* period with id {@code periodId} exist.
|
* period with id {@code periodId} exist.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
public MediaQueueItem getItem(int periodId) {
|
public MediaQueueItem getItem(int periodId) {
|
||||||
MediaStatus mediaStatus = getMediaStatus();
|
MediaStatus mediaStatus = getMediaStatus();
|
||||||
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
|
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
|
||||||
|
|
@ -257,9 +270,9 @@ public final class CastPlayer extends BasePlayer {
|
||||||
/**
|
/**
|
||||||
* Sets a listener for updates on the cast session availability.
|
* Sets a listener for updates on the cast session availability.
|
||||||
*
|
*
|
||||||
* @param listener The {@link SessionAvailabilityListener}.
|
* @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
|
||||||
*/
|
*/
|
||||||
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) {
|
public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
|
||||||
sessionAvailabilityListener = listener;
|
sessionAvailabilityListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,12 +309,17 @@ public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addListener(EventListener listener) {
|
public void addListener(EventListener listener) {
|
||||||
listeners.add(listener);
|
listeners.addIfAbsent(new ListenerHolder(listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeListener(EventListener listener) {
|
public void removeListener(EventListener listener) {
|
||||||
listeners.remove(listener);
|
for (ListenerHolder listenerHolder : listeners) {
|
||||||
|
if (listenerHolder.listener.equals(listener)) {
|
||||||
|
listenerHolder.release();
|
||||||
|
listeners.remove(listenerHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -311,6 +329,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public ExoPlaybackException getPlaybackError() {
|
public ExoPlaybackException getPlaybackError() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -348,14 +367,13 @@ public final class CastPlayer extends BasePlayer {
|
||||||
pendingSeekCount++;
|
pendingSeekCount++;
|
||||||
pendingSeekWindowIndex = windowIndex;
|
pendingSeekWindowIndex = windowIndex;
|
||||||
pendingSeekPositionMs = positionMs;
|
pendingSeekPositionMs = positionMs;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
|
new ListenerNotificationTask(
|
||||||
}
|
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
|
||||||
} else if (pendingSeekCount == 0) {
|
} else if (pendingSeekCount == 0) {
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||||
listener.onSeekProcessed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
flushNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -442,11 +460,6 @@ public final class CastPlayer extends BasePlayer {
|
||||||
return currentTimeline;
|
return currentTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable public Object getCurrentManifest() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCurrentPeriodIndex() {
|
public int getCurrentPeriodIndex() {
|
||||||
return getCurrentWindowIndex();
|
return getCurrentWindowIndex();
|
||||||
|
|
@ -519,7 +532,7 @@ public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
public void updateInternalState() {
|
private void updateInternalState() {
|
||||||
if (remoteMediaClient == null) {
|
if (remoteMediaClient == null) {
|
||||||
// There is no session. We leave the state of the player as it is now.
|
// There is no session. We leave the state of the player as it is now.
|
||||||
return;
|
return;
|
||||||
|
|
@ -531,30 +544,40 @@ public final class CastPlayer extends BasePlayer {
|
||||||
|| this.playWhenReady != playWhenReady) {
|
|| this.playWhenReady != playWhenReady) {
|
||||||
this.playbackState = playbackState;
|
this.playbackState = playbackState;
|
||||||
this.playWhenReady = playWhenReady;
|
this.playWhenReady = playWhenReady;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
|
new ListenerNotificationTask(
|
||||||
}
|
listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
|
||||||
}
|
}
|
||||||
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
|
||||||
if (this.repeatMode != repeatMode) {
|
if (this.repeatMode != repeatMode) {
|
||||||
this.repeatMode = repeatMode;
|
this.repeatMode = repeatMode;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onRepeatModeChanged(repeatMode);
|
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode)));
|
||||||
}
|
|
||||||
}
|
|
||||||
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
|
|
||||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
|
||||||
this.currentWindowIndex = currentWindowIndex;
|
|
||||||
for (EventListener listener : listeners) {
|
|
||||||
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updateTracksAndSelections()) {
|
|
||||||
for (EventListener listener : listeners) {
|
|
||||||
listener.onTracksChanged(currentTrackGroups, currentTrackSelection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
maybeUpdateTimelineAndNotify();
|
maybeUpdateTimelineAndNotify();
|
||||||
|
|
||||||
|
int currentWindowIndex = C.INDEX_UNSET;
|
||||||
|
MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
|
||||||
|
if (currentItem != null) {
|
||||||
|
currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
|
||||||
|
}
|
||||||
|
if (currentWindowIndex == C.INDEX_UNSET) {
|
||||||
|
// The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
|
||||||
|
currentWindowIndex = 0;
|
||||||
|
}
|
||||||
|
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||||
|
this.currentWindowIndex = currentWindowIndex;
|
||||||
|
notificationsBatch.add(
|
||||||
|
new ListenerNotificationTask(
|
||||||
|
listener ->
|
||||||
|
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
|
||||||
|
}
|
||||||
|
if (updateTracksAndSelections()) {
|
||||||
|
notificationsBatch.add(
|
||||||
|
new ListenerNotificationTask(
|
||||||
|
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
|
||||||
|
}
|
||||||
|
flushNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUpdateTimelineAndNotify() {
|
private void maybeUpdateTimelineAndNotify() {
|
||||||
|
|
@ -562,9 +585,9 @@ public final class CastPlayer extends BasePlayer {
|
||||||
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
@Player.TimelineChangeReason int reason = waitingForInitialTimeline
|
||||||
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
||||||
waitingForInitialTimeline = false;
|
waitingForInitialTimeline = false;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(
|
||||||
listener.onTimelineChanged(currentTimeline, null, reason);
|
new ListenerNotificationTask(
|
||||||
}
|
listener -> listener.onTimelineChanged(currentTimeline, reason)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -654,7 +677,8 @@ public final class CastPlayer extends BasePlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable MediaStatus getMediaStatus() {
|
@Nullable
|
||||||
|
private MediaStatus getMediaStatus() {
|
||||||
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -702,16 +726,6 @@ public final class CastPlayer extends BasePlayer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
|
|
||||||
* there is no media session, returns 0.
|
|
||||||
*/
|
|
||||||
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
|
|
||||||
Integer currentItemId = mediaStatus != null
|
|
||||||
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
|
|
||||||
return currentItemId != null ? currentItemId : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isTrackActive(long id, long[] activeTrackIds) {
|
private static boolean isTrackActive(long id, long[] activeTrackIds) {
|
||||||
for (long activeTrackId : activeTrackIds) {
|
for (long activeTrackId : activeTrackIds) {
|
||||||
if (activeTrackId == id) {
|
if (activeTrackId == id) {
|
||||||
|
|
@ -827,7 +841,23 @@ public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result callbacks hooks.
|
// Internal methods.
|
||||||
|
|
||||||
|
private void flushNotifications() {
|
||||||
|
boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty();
|
||||||
|
ongoingNotificationsTasks.addAll(notificationsBatch);
|
||||||
|
notificationsBatch.clear();
|
||||||
|
if (recursiveNotification) {
|
||||||
|
// This will be handled once the current notification task is finished.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (!ongoingNotificationsTasks.isEmpty()) {
|
||||||
|
ongoingNotificationsTasks.peekFirst().execute();
|
||||||
|
ongoingNotificationsTasks.removeFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal classes.
|
||||||
|
|
||||||
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
|
||||||
|
|
||||||
|
|
@ -841,9 +871,25 @@ public final class CastPlayer extends BasePlayer {
|
||||||
if (--pendingSeekCount == 0) {
|
if (--pendingSeekCount == 0) {
|
||||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||||
pendingSeekPositionMs = C.TIME_UNSET;
|
pendingSeekPositionMs = C.TIME_UNSET;
|
||||||
for (EventListener listener : listeners) {
|
notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
|
||||||
listener.onSeekProcessed();
|
flushNotifications();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ListenerNotificationTask {
|
||||||
|
|
||||||
|
private final Iterator<ListenerHolder> listenersSnapshot;
|
||||||
|
private final ListenerInvocation listenerInvocation;
|
||||||
|
|
||||||
|
private ListenerNotificationTask(ListenerInvocation listenerInvocation) {
|
||||||
|
this.listenersSnapshot = listeners.iterator();
|
||||||
|
this.listenerInvocation = listenerInvocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute() {
|
||||||
|
while (listenersSnapshot.hasNext()) {
|
||||||
|
listenersSnapshot.next().invoke(listenerInvocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
* @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;
|
||||||
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;
|
||||||
*/
|
*/
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,42 +17,31 @@ package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
|
|
||||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
|
||||||
|
|
||||||
/** Representation of an item that can be played by a media player. */
|
/** Representation of a media item. */
|
||||||
public final class MediaItem {
|
public final class MediaItem {
|
||||||
|
|
||||||
/** A builder for {@link MediaItem} instances. */
|
/** A builder for {@link MediaItem} instances. */
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
@Nullable private UUID uuid;
|
@Nullable private Uri uri;
|
||||||
private String title;
|
@Nullable private String title;
|
||||||
private String description;
|
@Nullable private String mimeType;
|
||||||
private MediaItem.UriBundle media;
|
@Nullable private DrmConfiguration drmConfiguration;
|
||||||
@Nullable private Object attachment;
|
|
||||||
private List<MediaItem.DrmScheme> drmSchemes;
|
|
||||||
private long startPositionUs;
|
|
||||||
private long endPositionUs;
|
|
||||||
private String mimeType;
|
|
||||||
|
|
||||||
/** Creates an builder with default field values. */
|
/** See {@link MediaItem#uri}. */
|
||||||
public Builder() {
|
public Builder setUri(String uri) {
|
||||||
clearInternal();
|
return setUri(Uri.parse(uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See {@link MediaItem#uuid}. */
|
/** See {@link MediaItem#uri}. */
|
||||||
public Builder setUuid(UUID uuid) {
|
public Builder setUri(Uri uri) {
|
||||||
this.uuid = uuid;
|
this.uri = uri;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,307 +51,125 @@ public final class MediaItem {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See {@link MediaItem#description}. */
|
|
||||||
public Builder setDescription(String description) {
|
|
||||||
this.description = description;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */
|
|
||||||
public Builder setMedia(String uri) {
|
|
||||||
return setMedia(new UriBundle(Uri.parse(uri)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link MediaItem#media}. */
|
|
||||||
public Builder setMedia(UriBundle media) {
|
|
||||||
this.media = media;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link MediaItem#attachment}. */
|
|
||||||
public Builder setAttachment(Object attachment) {
|
|
||||||
this.attachment = attachment;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link MediaItem#drmSchemes}. */
|
|
||||||
public Builder setDrmSchemes(List<MediaItem.DrmScheme> drmSchemes) {
|
|
||||||
this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link MediaItem#startPositionUs}. */
|
|
||||||
public Builder setStartPositionUs(long startPositionUs) {
|
|
||||||
this.startPositionUs = startPositionUs;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link MediaItem#endPositionUs}. */
|
|
||||||
public Builder setEndPositionUs(long endPositionUs) {
|
|
||||||
Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE);
|
|
||||||
this.endPositionUs = endPositionUs;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** See {@link MediaItem#mimeType}. */
|
/** See {@link MediaItem#mimeType}. */
|
||||||
public Builder setMimeType(String mimeType) {
|
public Builder setMimeType(String mimeType) {
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** See {@link MediaItem#drmConfiguration}. */
|
||||||
* Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the
|
public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
|
||||||
* {@link MediaItem}.
|
this.drmConfiguration = drmConfiguration;
|
||||||
*/
|
|
||||||
public MediaItem buildAndClear() {
|
|
||||||
MediaItem item = build();
|
|
||||||
clearInternal();
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the builder to default values. */
|
|
||||||
public Builder clear() {
|
|
||||||
clearInternal();
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns a new {@link MediaItem} instance with the current builder values. */
|
||||||
* Returns a new {@link MediaItem} instance with the current builder values. This method also
|
|
||||||
* clears any values passed to {@link #setUuid(UUID)}.
|
|
||||||
*/
|
|
||||||
public MediaItem build() {
|
public MediaItem build() {
|
||||||
UUID uuid = this.uuid;
|
Assertions.checkNotNull(uri);
|
||||||
this.uuid = null;
|
return new MediaItem(uri, title, mimeType, drmConfiguration);
|
||||||
return new MediaItem(
|
|
||||||
uuid != null ? uuid : UUID.randomUUID(),
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
media,
|
|
||||||
attachment,
|
|
||||||
drmSchemes,
|
|
||||||
startPositionUs,
|
|
||||||
endPositionUs,
|
|
||||||
mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnsuresNonNull({"title", "description", "media", "drmSchemes", "mimeType"})
|
|
||||||
private void clearInternal(@UnknownInitialization Builder this) {
|
|
||||||
uuid = null;
|
|
||||||
title = "";
|
|
||||||
description = "";
|
|
||||||
media = UriBundle.EMPTY;
|
|
||||||
attachment = null;
|
|
||||||
drmSchemes = Collections.emptyList();
|
|
||||||
startPositionUs = C.TIME_UNSET;
|
|
||||||
endPositionUs = C.TIME_UNSET;
|
|
||||||
mimeType = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bundles a resource's URI with headers to attach to any request to that URI. */
|
/** DRM configuration for a media item. */
|
||||||
public static final class UriBundle {
|
public static final class DrmConfiguration {
|
||||||
|
|
||||||
/** An empty {@link UriBundle}. */
|
|
||||||
public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY);
|
|
||||||
|
|
||||||
/** A URI. */
|
|
||||||
public final Uri uri;
|
|
||||||
|
|
||||||
/** The headers to attach to any request for the given URI. */
|
|
||||||
public final Map<String, String> requestHeaders;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance with no request headers.
|
|
||||||
*
|
|
||||||
* @param uri See {@link #uri}.
|
|
||||||
*/
|
|
||||||
public UriBundle(Uri uri) {
|
|
||||||
this(uri, Collections.emptyMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an instance with the given URI and request headers.
|
|
||||||
*
|
|
||||||
* @param uri See {@link #uri}.
|
|
||||||
* @param requestHeaders See {@link #requestHeaders}.
|
|
||||||
*/
|
|
||||||
public UriBundle(Uri uri, Map<String, String> requestHeaders) {
|
|
||||||
this.uri = uri;
|
|
||||||
this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(@Nullable Object other) {
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (other == null || getClass() != other.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
UriBundle uriBundle = (UriBundle) other;
|
|
||||||
return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = uri.hashCode();
|
|
||||||
result = 31 * result + requestHeaders.hashCode();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a DRM protection scheme, and optionally provides information about how to acquire
|
|
||||||
* the license for the media.
|
|
||||||
*/
|
|
||||||
public static final class DrmScheme {
|
|
||||||
|
|
||||||
/** The UUID of the protection scheme. */
|
/** The UUID of the protection scheme. */
|
||||||
public final UUID uuid;
|
public final UUID uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional {@link UriBundle} for the license server. If no license server is provided, the
|
* Optional license server {@link Uri}. If {@code null} then the license server must be
|
||||||
* server must be provided by the media.
|
* specified by the media.
|
||||||
*/
|
*/
|
||||||
@Nullable public final UriBundle licenseServer;
|
@Nullable public final Uri licenseUri;
|
||||||
|
|
||||||
|
/** Headers that should be attached to any license requests. */
|
||||||
|
public final Map<String, String> requestHeaders;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance.
|
||||||
*
|
*
|
||||||
* @param uuid See {@link #uuid}.
|
* @param uuid See {@link #uuid}.
|
||||||
* @param licenseServer See {@link #licenseServer}.
|
* @param licenseUri See {@link #licenseUri}.
|
||||||
|
* @param requestHeaders See {@link #requestHeaders}.
|
||||||
*/
|
*/
|
||||||
public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) {
|
public DrmConfiguration(
|
||||||
|
UUID uuid, @Nullable Uri licenseUri, @Nullable Map<String, String> requestHeaders) {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.licenseServer = licenseServer;
|
this.licenseUri = licenseUri;
|
||||||
|
this.requestHeaders =
|
||||||
|
requestHeaders == null
|
||||||
|
? Collections.emptyMap()
|
||||||
|
: Collections.unmodifiableMap(requestHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(@Nullable Object other) {
|
public boolean equals(@Nullable Object obj) {
|
||||||
if (this == other) {
|
if (this == obj) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (other == null || getClass() != other.getClass()) {
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrmScheme drmScheme = (DrmScheme) other;
|
DrmConfiguration other = (DrmConfiguration) obj;
|
||||||
return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer);
|
return uuid.equals(other.uuid)
|
||||||
|
&& Util.areEqual(licenseUri, other.licenseUri)
|
||||||
|
&& requestHeaders.equals(other.requestHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = uuid.hashCode();
|
int result = uuid.hashCode();
|
||||||
result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0);
|
result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
|
||||||
|
result = 31 * result + requestHeaders.hashCode();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The media {@link Uri}. */
|
||||||
* A UUID that identifies this item, potentially across different devices. The default value is
|
public final Uri uri;
|
||||||
* obtained by calling {@link UUID#randomUUID()}.
|
|
||||||
*/
|
|
||||||
public final UUID uuid;
|
|
||||||
|
|
||||||
/** The title of the item. The default value is an empty string. */
|
/** The title of the item, or {@code null} if unspecified. */
|
||||||
public final String title;
|
@Nullable public final String title;
|
||||||
|
|
||||||
/** A description for the item. The default value is an empty string. */
|
/** The mime type for the media, or {@code null} if unspecified. */
|
||||||
public final String description;
|
@Nullable public final String mimeType;
|
||||||
|
|
||||||
/**
|
/** Optional {@link DrmConfiguration} for the media. */
|
||||||
* A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}.
|
@Nullable public final DrmConfiguration drmConfiguration;
|
||||||
*/
|
|
||||||
public final UriBundle media;
|
|
||||||
|
|
||||||
/**
|
private MediaItem(
|
||||||
* An optional opaque object to attach to the media item. Handling of this attachment is
|
Uri uri,
|
||||||
* implementation specific. The default value is null.
|
@Nullable String title,
|
||||||
*/
|
@Nullable String mimeType,
|
||||||
@Nullable public final Object attachment;
|
@Nullable DrmConfiguration drmConfiguration) {
|
||||||
|
this.uri = uri;
|
||||||
/**
|
this.title = title;
|
||||||
* Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The
|
this.mimeType = mimeType;
|
||||||
* default value is an empty list.
|
this.drmConfiguration = drmConfiguration;
|
||||||
*/
|
}
|
||||||
public final List<DrmScheme> drmSchemes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The position in microseconds at which playback of this media item should start. {@link
|
|
||||||
* C#TIME_UNSET} if playback should start at the default position. The default value is {@link
|
|
||||||
* C#TIME_UNSET}.
|
|
||||||
*/
|
|
||||||
public final long startPositionUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The position in microseconds at which playback of this media item should end. {@link
|
|
||||||
* C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link
|
|
||||||
* C#TIME_UNSET}.
|
|
||||||
*/
|
|
||||||
public final long endPositionUs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The mime type of this media item. The default value is an empty string.
|
|
||||||
*
|
|
||||||
* <p>The usage of this mime type is optional and player implementation specific.
|
|
||||||
*/
|
|
||||||
public final String mimeType;
|
|
||||||
|
|
||||||
// TODO: Add support for sideloaded tracks, artwork, icon, and subtitle.
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(@Nullable Object other) {
|
public boolean equals(@Nullable Object obj) {
|
||||||
if (this == other) {
|
if (this == obj) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (other == null || getClass() != other.getClass()) {
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MediaItem mediaItem = (MediaItem) other;
|
MediaItem other = (MediaItem) obj;
|
||||||
return startPositionUs == mediaItem.startPositionUs
|
return uri.equals(other.uri)
|
||||||
&& endPositionUs == mediaItem.endPositionUs
|
&& Util.areEqual(title, other.title)
|
||||||
&& uuid.equals(mediaItem.uuid)
|
&& Util.areEqual(mimeType, other.mimeType)
|
||||||
&& title.equals(mediaItem.title)
|
&& Util.areEqual(drmConfiguration, other.drmConfiguration);
|
||||||
&& description.equals(mediaItem.description)
|
|
||||||
&& media.equals(mediaItem.media)
|
|
||||||
&& Util.areEqual(attachment, mediaItem.attachment)
|
|
||||||
&& drmSchemes.equals(mediaItem.drmSchemes)
|
|
||||||
&& mimeType.equals(mediaItem.mimeType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int result = uuid.hashCode();
|
int result = uri.hashCode();
|
||||||
result = 31 * result + title.hashCode();
|
result = 31 * result + (title == null ? 0 : title.hashCode());
|
||||||
result = 31 * result + description.hashCode();
|
result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
|
||||||
result = 31 * result + media.hashCode();
|
result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
|
||||||
result = 31 * result + (attachment != null ? attachment.hashCode() : 0);
|
|
||||||
result = 31 * result + drmSchemes.hashCode();
|
|
||||||
result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32));
|
|
||||||
result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32));
|
|
||||||
result = 31 * result + mimeType.hashCode();
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaItem(
|
|
||||||
UUID uuid,
|
|
||||||
String title,
|
|
||||||
String description,
|
|
||||||
UriBundle media,
|
|
||||||
@Nullable Object attachment,
|
|
||||||
List<DrmScheme> drmSchemes,
|
|
||||||
long startPositionUs,
|
|
||||||
long endPositionUs,
|
|
||||||
String mimeType) {
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.title = title;
|
|
||||||
this.description = description;
|
|
||||||
this.media = media;
|
|
||||||
this.attachment = attachment;
|
|
||||||
this.drmSchemes = drmSchemes;
|
|
||||||
this.startPositionUs = startPositionUs;
|
|
||||||
this.endPositionUs = endPositionUs;
|
|
||||||
this.mimeType = mimeType;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
/*
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -32,113 +29,58 @@ import org.junit.runner.RunWith;
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class MediaItemTest {
|
public class MediaItemTest {
|
||||||
|
|
||||||
@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
|
@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
|
|
||||||
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
|
@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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
@ -41,6 +43,7 @@ import java.util.Map.Entry;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||||
import org.chromium.net.CronetEngine;
|
import org.chromium.net.CronetEngine;
|
||||||
import org.chromium.net.CronetException;
|
import org.chromium.net.CronetException;
|
||||||
import org.chromium.net.NetworkException;
|
import org.chromium.net.NetworkException;
|
||||||
|
|
@ -113,16 +116,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
|
|
||||||
private final CronetEngine cronetEngine;
|
private final CronetEngine cronetEngine;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
@Nullable private final Predicate<String> contentTypePredicate;
|
|
||||||
private final int connectTimeoutMs;
|
private final int connectTimeoutMs;
|
||||||
private final int readTimeoutMs;
|
private final int readTimeoutMs;
|
||||||
private final boolean resetTimeoutOnRedirects;
|
private final boolean resetTimeoutOnRedirects;
|
||||||
private final boolean handleSetCookieRequests;
|
private final boolean handleSetCookieRequests;
|
||||||
private final RequestProperties defaultRequestProperties;
|
@Nullable private final RequestProperties defaultRequestProperties;
|
||||||
private final RequestProperties requestProperties;
|
private final RequestProperties requestProperties;
|
||||||
private final ConditionVariable operation;
|
private final ConditionVariable operation;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
|
@Nullable private Predicate<String> contentTypePredicate;
|
||||||
|
|
||||||
// Accessed by the calling thread only.
|
// Accessed by the calling thread only.
|
||||||
private boolean opened;
|
private boolean opened;
|
||||||
private long bytesToSkip;
|
private long bytesToSkip;
|
||||||
|
|
@ -130,18 +134,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
|
|
||||||
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
|
// Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible
|
||||||
// to reads made by the Cronet thread.
|
// to reads made by the Cronet thread.
|
||||||
private UrlRequest currentUrlRequest;
|
@Nullable private UrlRequest currentUrlRequest;
|
||||||
private DataSpec currentDataSpec;
|
@Nullable private DataSpec currentDataSpec;
|
||||||
|
|
||||||
// Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
|
// Reference written and read by calling thread only. Passed to Cronet thread as a local variable.
|
||||||
// operation.open() calls ensure writes into the buffer are visible to reads made by the calling
|
// operation.open() calls ensure writes into the buffer are visible to reads made by the calling
|
||||||
// thread.
|
// thread.
|
||||||
private ByteBuffer readBuffer;
|
@Nullable private ByteBuffer readBuffer;
|
||||||
|
|
||||||
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
|
// Written from the Cronet thread only. operation.open() calls ensure writes are visible to reads
|
||||||
// made by the calling thread.
|
// made by the calling thread.
|
||||||
private UrlResponseInfo responseInfo;
|
@Nullable private UrlResponseInfo responseInfo;
|
||||||
private IOException exception;
|
@Nullable private IOException exception;
|
||||||
private boolean finished;
|
private boolean finished;
|
||||||
|
|
||||||
private volatile long currentConnectTimeoutMs;
|
private volatile long currentConnectTimeoutMs;
|
||||||
|
|
@ -155,7 +159,78 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
* handling is a fast operation when using a direct executor.
|
* handling is a fast operation when using a direct executor.
|
||||||
*/
|
*/
|
||||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
|
public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
|
||||||
this(cronetEngine, executor, /* contentTypePredicate= */ null);
|
this(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
/* resetTimeoutOnRedirects= */ false,
|
||||||
|
/* defaultRequestProperties= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cronetEngine A CronetEngine.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||||
|
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||||
|
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||||
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
|
* handling is a fast operation when using a direct executor.
|
||||||
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||||
|
* server as HTTP headers on every request.
|
||||||
|
*/
|
||||||
|
public CronetDataSource(
|
||||||
|
CronetEngine cronetEngine,
|
||||||
|
Executor executor,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
@Nullable RequestProperties defaultRequestProperties) {
|
||||||
|
this(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
Clock.DEFAULT,
|
||||||
|
defaultRequestProperties,
|
||||||
|
/* handleSetCookieRequests= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cronetEngine A CronetEngine.
|
||||||
|
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||||
|
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||||
|
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||||
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
|
* handling is a fast operation when using a direct executor.
|
||||||
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||||
|
* server as HTTP headers on every request.
|
||||||
|
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||||
|
* the redirect url in the "Cookie" header.
|
||||||
|
*/
|
||||||
|
public CronetDataSource(
|
||||||
|
CronetEngine cronetEngine,
|
||||||
|
Executor executor,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
@Nullable RequestProperties defaultRequestProperties,
|
||||||
|
boolean handleSetCookieRequests) {
|
||||||
|
this(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
Clock.DEFAULT,
|
||||||
|
defaultRequestProperties,
|
||||||
|
handleSetCookieRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -168,7 +243,10 @@ public class CronetDataSource 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 an {@link InvalidContentTypeException} is thrown from {@link
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
* #open(DataSpec)}.
|
* #open(DataSpec)}.
|
||||||
|
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
|
||||||
|
* #setContentTypePredicate(Predicate)}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public CronetDataSource(
|
public CronetDataSource(
|
||||||
CronetEngine cronetEngine,
|
CronetEngine cronetEngine,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
|
|
@ -179,9 +257,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
contentTypePredicate,
|
contentTypePredicate,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
false,
|
/* resetTimeoutOnRedirects= */ false,
|
||||||
null,
|
/* defaultRequestProperties= */ null);
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -197,8 +274,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
* @param defaultRequestProperties The default request properties to be used.
|
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||||
|
* server as HTTP headers on every request.
|
||||||
|
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||||
|
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public CronetDataSource(
|
public CronetDataSource(
|
||||||
CronetEngine cronetEngine,
|
CronetEngine cronetEngine,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
|
|
@ -206,7 +287,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
boolean resetTimeoutOnRedirects,
|
boolean resetTimeoutOnRedirects,
|
||||||
RequestProperties defaultRequestProperties) {
|
@Nullable RequestProperties defaultRequestProperties) {
|
||||||
this(
|
this(
|
||||||
cronetEngine,
|
cronetEngine,
|
||||||
executor,
|
executor,
|
||||||
|
|
@ -214,9 +295,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
connectTimeoutMs,
|
connectTimeoutMs,
|
||||||
readTimeoutMs,
|
readTimeoutMs,
|
||||||
resetTimeoutOnRedirects,
|
resetTimeoutOnRedirects,
|
||||||
Clock.DEFAULT,
|
|
||||||
defaultRequestProperties,
|
defaultRequestProperties,
|
||||||
false);
|
/* handleSetCookieRequests= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -232,10 +312,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
* @param defaultRequestProperties The default request properties to be used.
|
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
|
||||||
|
* server as HTTP headers on every request.
|
||||||
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
* @param handleSetCookieRequests Whether "Set-Cookie" requests on redirect should be forwarded to
|
||||||
* the redirect url in the "Cookie" header.
|
* the redirect url in the "Cookie" header.
|
||||||
|
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||||
|
* RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public CronetDataSource(
|
public CronetDataSource(
|
||||||
CronetEngine cronetEngine,
|
CronetEngine cronetEngine,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
|
|
@ -243,35 +327,33 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
boolean resetTimeoutOnRedirects,
|
boolean resetTimeoutOnRedirects,
|
||||||
RequestProperties defaultRequestProperties,
|
@Nullable RequestProperties defaultRequestProperties,
|
||||||
boolean handleSetCookieRequests) {
|
boolean handleSetCookieRequests) {
|
||||||
this(
|
this(
|
||||||
cronetEngine,
|
cronetEngine,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
connectTimeoutMs,
|
connectTimeoutMs,
|
||||||
readTimeoutMs,
|
readTimeoutMs,
|
||||||
resetTimeoutOnRedirects,
|
resetTimeoutOnRedirects,
|
||||||
Clock.DEFAULT,
|
Clock.DEFAULT,
|
||||||
defaultRequestProperties,
|
defaultRequestProperties,
|
||||||
handleSetCookieRequests);
|
handleSetCookieRequests);
|
||||||
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ CronetDataSource(
|
/* package */ CronetDataSource(
|
||||||
CronetEngine cronetEngine,
|
CronetEngine cronetEngine,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
@Nullable Predicate<String> contentTypePredicate,
|
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
boolean resetTimeoutOnRedirects,
|
boolean resetTimeoutOnRedirects,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
RequestProperties defaultRequestProperties,
|
@Nullable RequestProperties defaultRequestProperties,
|
||||||
boolean handleSetCookieRequests) {
|
boolean handleSetCookieRequests) {
|
||||||
super(/* isNetwork= */ true);
|
super(/* isNetwork= */ true);
|
||||||
this.urlRequestCallback = new UrlRequestCallback();
|
this.urlRequestCallback = new UrlRequestCallback();
|
||||||
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
||||||
this.executor = Assertions.checkNotNull(executor);
|
this.executor = Assertions.checkNotNull(executor);
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
|
||||||
this.connectTimeoutMs = connectTimeoutMs;
|
this.connectTimeoutMs = connectTimeoutMs;
|
||||||
this.readTimeoutMs = readTimeoutMs;
|
this.readTimeoutMs = readTimeoutMs;
|
||||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||||
|
|
@ -282,6 +364,17 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
operation = new ConditionVariable();
|
operation = new ConditionVariable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
// HttpDataSource implementation.
|
// HttpDataSource implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -305,6 +398,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public Uri getUri() {
|
public Uri getUri() {
|
||||||
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
|
return responseInfo == null ? null : Uri.parse(responseInfo.getUrl());
|
||||||
}
|
}
|
||||||
|
|
@ -317,22 +411,23 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
operation.close();
|
operation.close();
|
||||||
resetConnectTimeout();
|
resetConnectTimeout();
|
||||||
currentDataSpec = dataSpec;
|
currentDataSpec = dataSpec;
|
||||||
|
UrlRequest urlRequest;
|
||||||
try {
|
try {
|
||||||
currentUrlRequest = buildRequestBuilder(dataSpec).build();
|
urlRequest = buildRequestBuilder(dataSpec).build();
|
||||||
|
currentUrlRequest = urlRequest;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new OpenException(e, currentDataSpec, Status.IDLE);
|
throw new OpenException(e, dataSpec, Status.IDLE);
|
||||||
}
|
}
|
||||||
currentUrlRequest.start();
|
urlRequest.start();
|
||||||
|
|
||||||
transferInitializing(dataSpec);
|
transferInitializing(dataSpec);
|
||||||
try {
|
try {
|
||||||
boolean connectionOpened = blockUntilConnectTimeout();
|
boolean connectionOpened = blockUntilConnectTimeout();
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
throw new OpenException(exception, currentDataSpec, getStatus(currentUrlRequest));
|
throw new OpenException(exception, dataSpec, getStatus(urlRequest));
|
||||||
} else if (!connectionOpened) {
|
} else if (!connectionOpened) {
|
||||||
// The timeout was reached before the connection was opened.
|
// The timeout was reached before the connection was opened.
|
||||||
throw new OpenException(
|
throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
|
||||||
new SocketTimeoutException(), dataSpec, getStatus(currentUrlRequest));
|
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
|
|
@ -340,6 +435,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a valid response code.
|
// Check for a valid response code.
|
||||||
|
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
|
||||||
int responseCode = responseInfo.getHttpStatusCode();
|
int responseCode = responseInfo.getHttpStatusCode();
|
||||||
if (responseCode < 200 || responseCode > 299) {
|
if (responseCode < 200 || responseCode > 299) {
|
||||||
InvalidResponseCodeException exception =
|
InvalidResponseCodeException exception =
|
||||||
|
|
@ -347,7 +443,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
responseCode,
|
responseCode,
|
||||||
responseInfo.getHttpStatusText(),
|
responseInfo.getHttpStatusText(),
|
||||||
responseInfo.getAllHeaders(),
|
responseInfo.getAllHeaders(),
|
||||||
currentDataSpec);
|
dataSpec);
|
||||||
if (responseCode == 416) {
|
if (responseCode == 416) {
|
||||||
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
|
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
|
||||||
}
|
}
|
||||||
|
|
@ -355,11 +451,12 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a valid content type.
|
// Check for a valid content type.
|
||||||
|
Predicate<String> contentTypePredicate = this.contentTypePredicate;
|
||||||
if (contentTypePredicate != null) {
|
if (contentTypePredicate != null) {
|
||||||
List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
|
List<String> contentTypeHeaders = responseInfo.getAllHeaders().get(CONTENT_TYPE);
|
||||||
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
|
String contentType = isEmpty(contentTypeHeaders) ? null : contentTypeHeaders.get(0);
|
||||||
if (!contentTypePredicate.evaluate(contentType)) {
|
if (contentType != null && !contentTypePredicate.evaluate(contentType)) {
|
||||||
throw new InvalidContentTypeException(contentType, currentDataSpec);
|
throw new InvalidContentTypeException(contentType, dataSpec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,7 +466,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
|
||||||
|
|
||||||
// Calculate the content length.
|
// Calculate the content length.
|
||||||
if (!getIsCompressed(responseInfo)) {
|
if (!isCompressed(responseInfo)) {
|
||||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||||
bytesRemaining = dataSpec.length;
|
bytesRemaining = dataSpec.length;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -378,7 +475,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
} else {
|
} else {
|
||||||
// If the response is compressed then the content length will be that of the compressed data
|
// If the response is compressed then the content length will be that of the compressed data
|
||||||
// which isn't what we want. Always use the dataSpec length in this case.
|
// which isn't what we want. Always use the dataSpec length in this case.
|
||||||
bytesRemaining = currentDataSpec.length;
|
bytesRemaining = dataSpec.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
opened = true;
|
opened = true;
|
||||||
|
|
@ -397,37 +494,19 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
return C.RESULT_END_OF_INPUT;
|
return C.RESULT_END_OF_INPUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ByteBuffer readBuffer = this.readBuffer;
|
||||||
if (readBuffer == null) {
|
if (readBuffer == null) {
|
||||||
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||||
readBuffer.limit(0);
|
readBuffer.limit(0);
|
||||||
|
this.readBuffer = readBuffer;
|
||||||
}
|
}
|
||||||
while (!readBuffer.hasRemaining()) {
|
while (!readBuffer.hasRemaining()) {
|
||||||
// Fill readBuffer with more data from Cronet.
|
// Fill readBuffer with more data from Cronet.
|
||||||
operation.close();
|
operation.close();
|
||||||
readBuffer.clear();
|
readBuffer.clear();
|
||||||
currentUrlRequest.read(readBuffer);
|
readInternal(castNonNull(readBuffer));
|
||||||
try {
|
|
||||||
if (!operation.block(readTimeoutMs)) {
|
|
||||||
throw new SocketTimeoutException();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
|
||||||
// operation during a subsequent request.
|
|
||||||
readBuffer = null;
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new HttpDataSourceException(
|
|
||||||
new InterruptedIOException(e), currentDataSpec, HttpDataSourceException.TYPE_READ);
|
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
// The operation is ongoing so replace readBuffer to avoid it being written to by this
|
|
||||||
// operation during a subsequent request.
|
|
||||||
readBuffer = null;
|
|
||||||
throw new HttpDataSourceException(e, currentDataSpec, HttpDataSourceException.TYPE_READ);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exception != null) {
|
if (finished) {
|
||||||
throw new HttpDataSourceException(exception, currentDataSpec,
|
|
||||||
HttpDataSourceException.TYPE_READ);
|
|
||||||
} else if (finished) {
|
|
||||||
bytesRemaining = 0;
|
bytesRemaining = 0;
|
||||||
return C.RESULT_END_OF_INPUT;
|
return C.RESULT_END_OF_INPUT;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -452,6 +531,115 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
return bytesRead;
|
return bytesRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads up to {@code buffer.remaining()} bytes of data and stores them into {@code buffer},
|
||||||
|
* starting at {@code buffer.position()}. Advances the position of the buffer by the number of
|
||||||
|
* bytes read and returns this length.
|
||||||
|
*
|
||||||
|
* <p>If there is an error, a {@link HttpDataSourceException} is thrown and the contents of {@code
|
||||||
|
* buffer} should be ignored. If the exception has error code {@code
|
||||||
|
* HttpDataSourceException.TYPE_READ}, note that Cronet may continue writing into {@code buffer}
|
||||||
|
* after the method has returned. Thus the caller should not attempt to reuse the buffer.
|
||||||
|
*
|
||||||
|
* <p>If {@code buffer.remaining()} is zero then 0 is returned. Otherwise, if no data is available
|
||||||
|
* because the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is
|
||||||
|
* returned. Otherwise, the call will block until at least one byte of data has been read and the
|
||||||
|
* number of bytes read is returned.
|
||||||
|
*
|
||||||
|
* <p>Passed buffer must be direct ByteBuffer. If you have a non-direct ByteBuffer, consider the
|
||||||
|
* alternative read method with its backed array.
|
||||||
|
*
|
||||||
|
* @param buffer The ByteBuffer into which the read data should be stored. Must be a direct
|
||||||
|
* ByteBuffer.
|
||||||
|
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if no data is available
|
||||||
|
* because the end of the opened range has been reached.
|
||||||
|
* @throws HttpDataSourceException If an error occurs reading from the source.
|
||||||
|
* @throws IllegalArgumentException If {@code buffer} is not a direct ByteBuffer.
|
||||||
|
*/
|
||||||
|
public int read(ByteBuffer buffer) throws HttpDataSourceException {
|
||||||
|
Assertions.checkState(opened);
|
||||||
|
|
||||||
|
if (!buffer.isDirect()) {
|
||||||
|
throw new IllegalArgumentException("Passed buffer is not a direct ByteBuffer");
|
||||||
|
}
|
||||||
|
if (!buffer.hasRemaining()) {
|
||||||
|
return 0;
|
||||||
|
} else if (bytesRemaining == 0) {
|
||||||
|
return C.RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
int readLength = buffer.remaining();
|
||||||
|
|
||||||
|
if (readBuffer != null) {
|
||||||
|
// Skip all the bytes we can from readBuffer if there are still bytes to skip.
|
||||||
|
if (bytesToSkip != 0) {
|
||||||
|
if (bytesToSkip >= readBuffer.remaining()) {
|
||||||
|
bytesToSkip -= readBuffer.remaining();
|
||||||
|
readBuffer.position(readBuffer.limit());
|
||||||
|
} else {
|
||||||
|
readBuffer.position(readBuffer.position() + (int) bytesToSkip);
|
||||||
|
bytesToSkip = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is existing data in the readBuffer, read as much as possible. Return if any read.
|
||||||
|
int copyBytes = copyByteBuffer(/* src= */ readBuffer, /* dst= */ buffer);
|
||||||
|
if (copyBytes != 0) {
|
||||||
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
|
bytesRemaining -= copyBytes;
|
||||||
|
}
|
||||||
|
bytesTransferred(copyBytes);
|
||||||
|
return copyBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean readMore = true;
|
||||||
|
while (readMore) {
|
||||||
|
// If bytesToSkip > 0, read into intermediate buffer that we can discard instead of caller's
|
||||||
|
// buffer. If we do not need to skip bytes, we may write to buffer directly.
|
||||||
|
final boolean useCallerBuffer = bytesToSkip == 0;
|
||||||
|
|
||||||
|
operation.close();
|
||||||
|
|
||||||
|
if (!useCallerBuffer) {
|
||||||
|
if (readBuffer == null) {
|
||||||
|
readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES);
|
||||||
|
} else {
|
||||||
|
readBuffer.clear();
|
||||||
|
}
|
||||||
|
if (bytesToSkip < READ_BUFFER_SIZE_BYTES) {
|
||||||
|
readBuffer.limit((int) bytesToSkip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill buffer with more data from Cronet.
|
||||||
|
readInternal(useCallerBuffer ? buffer : castNonNull(readBuffer));
|
||||||
|
|
||||||
|
if (finished) {
|
||||||
|
bytesRemaining = 0;
|
||||||
|
return C.RESULT_END_OF_INPUT;
|
||||||
|
} else {
|
||||||
|
// The operation didn't time out, fail or finish, and therefore data must have been read.
|
||||||
|
Assertions.checkState(
|
||||||
|
useCallerBuffer
|
||||||
|
? readLength > buffer.remaining()
|
||||||
|
: castNonNull(readBuffer).position() > 0);
|
||||||
|
// If we meant to skip bytes, subtract what was left and repeat, otherwise, continue.
|
||||||
|
if (useCallerBuffer) {
|
||||||
|
readMore = false;
|
||||||
|
} else {
|
||||||
|
bytesToSkip -= castNonNull(readBuffer).position();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int bytesRead = readLength - buffer.remaining();
|
||||||
|
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||||
|
bytesRemaining -= bytesRead;
|
||||||
|
}
|
||||||
|
bytesTransferred(bytesRead);
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void close() {
|
public synchronized void close() {
|
||||||
if (currentUrlRequest != null) {
|
if (currentUrlRequest != null) {
|
||||||
|
|
@ -524,7 +712,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
requestBuilder.addHeader("Range", rangeValue.toString());
|
requestBuilder.addHeader("Range", rangeValue.toString());
|
||||||
}
|
}
|
||||||
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed
|
// TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=711810 is fixed
|
||||||
// (adjusting the code as necessary).
|
// (adjusting the code as necessary).
|
||||||
// Force identity encoding unless gzip is allowed.
|
// Force identity encoding unless gzip is allowed.
|
||||||
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
|
// if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
|
||||||
|
|
@ -553,7 +741,49 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
|
currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean getIsCompressed(UrlResponseInfo info) {
|
/**
|
||||||
|
* Reads up to {@code buffer.remaining()} bytes of data from {@code currentUrlRequest} and stores
|
||||||
|
* them into {@code buffer}. If there is an error and {@code buffer == readBuffer}, then it resets
|
||||||
|
* the current {@code readBuffer} object so that it is not reused in the future.
|
||||||
|
*
|
||||||
|
* @param buffer The ByteBuffer into which the read data is stored. Must be a direct ByteBuffer.
|
||||||
|
* @throws HttpDataSourceException If an error occurs reading from the source.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("ReferenceEquality")
|
||||||
|
private void readInternal(ByteBuffer buffer) throws HttpDataSourceException {
|
||||||
|
castNonNull(currentUrlRequest).read(buffer);
|
||||||
|
try {
|
||||||
|
if (!operation.block(readTimeoutMs)) {
|
||||||
|
throw new SocketTimeoutException();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// The operation is ongoing so replace buffer to avoid it being written to by this
|
||||||
|
// operation during a subsequent request.
|
||||||
|
if (buffer == readBuffer) {
|
||||||
|
readBuffer = null;
|
||||||
|
}
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new HttpDataSourceException(
|
||||||
|
new InterruptedIOException(e),
|
||||||
|
castNonNull(currentDataSpec),
|
||||||
|
HttpDataSourceException.TYPE_READ);
|
||||||
|
} catch (SocketTimeoutException e) {
|
||||||
|
// The operation is ongoing so replace buffer to avoid it being written to by this
|
||||||
|
// operation during a subsequent request.
|
||||||
|
if (buffer == readBuffer) {
|
||||||
|
readBuffer = null;
|
||||||
|
}
|
||||||
|
throw new HttpDataSourceException(
|
||||||
|
e, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception != null) {
|
||||||
|
throw new HttpDataSourceException(
|
||||||
|
exception, castNonNull(currentDataSpec), HttpDataSourceException.TYPE_READ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCompressed(UrlResponseInfo info) {
|
||||||
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
|
for (Map.Entry<String, String> entry : info.getAllHeadersAsList()) {
|
||||||
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
|
if (entry.getKey().equalsIgnoreCase("Content-Encoding")) {
|
||||||
return !entry.getValue().equalsIgnoreCase("identity");
|
return !entry.getValue().equalsIgnoreCase("identity");
|
||||||
|
|
@ -631,10 +861,22 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
return statusHolder[0];
|
return statusHolder[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isEmpty(List<?> list) {
|
@EnsuresNonNullIf(result = false, expression = "#1")
|
||||||
|
private static boolean isEmpty(@Nullable List<?> list) {
|
||||||
return list == null || list.isEmpty();
|
return list == null || list.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy as much as possible from the src buffer into dst buffer.
|
||||||
|
// Returns the number of bytes copied.
|
||||||
|
private static int copyByteBuffer(ByteBuffer src, ByteBuffer dst) {
|
||||||
|
int remaining = Math.min(src.remaining(), dst.remaining());
|
||||||
|
int limit = src.limit();
|
||||||
|
src.limit(src.position() + remaining);
|
||||||
|
dst.put(src);
|
||||||
|
src.limit(limit);
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
|
||||||
private final class UrlRequestCallback extends UrlRequest.Callback {
|
private final class UrlRequestCallback extends UrlRequest.Callback {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -643,13 +885,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
if (request != currentUrlRequest) {
|
if (request != currentUrlRequest) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest);
|
||||||
|
DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
|
||||||
|
if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||||
int responseCode = info.getHttpStatusCode();
|
int responseCode = info.getHttpStatusCode();
|
||||||
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
|
// The industry standard is to disregard POST redirects when the status code is 307 or 308.
|
||||||
if (responseCode == 307 || responseCode == 308) {
|
if (responseCode == 307 || responseCode == 308) {
|
||||||
exception =
|
exception =
|
||||||
new InvalidResponseCodeException(
|
new InvalidResponseCodeException(
|
||||||
responseCode, info.getHttpStatusText(), info.getAllHeaders(), currentDataSpec);
|
responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
|
||||||
operation.open();
|
operation.open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -658,40 +902,46 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
resetConnectTimeout();
|
resetConnectTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<String>> headers = info.getAllHeaders();
|
if (!handleSetCookieRequests) {
|
||||||
if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
|
|
||||||
request.followRedirect();
|
request.followRedirect();
|
||||||
} else {
|
return;
|
||||||
currentUrlRequest.cancel();
|
|
||||||
DataSpec redirectUrlDataSpec;
|
|
||||||
if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
|
||||||
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
|
||||||
// transformed into a GET.
|
|
||||||
redirectUrlDataSpec =
|
|
||||||
new DataSpec(
|
|
||||||
Uri.parse(newLocationUrl),
|
|
||||||
DataSpec.HTTP_METHOD_GET,
|
|
||||||
/* httpBody= */ null,
|
|
||||||
currentDataSpec.absoluteStreamPosition,
|
|
||||||
currentDataSpec.position,
|
|
||||||
currentDataSpec.length,
|
|
||||||
currentDataSpec.key,
|
|
||||||
currentDataSpec.flags);
|
|
||||||
} else {
|
|
||||||
redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
|
|
||||||
}
|
|
||||||
UrlRequest.Builder requestBuilder;
|
|
||||||
try {
|
|
||||||
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
|
|
||||||
} catch (IOException e) {
|
|
||||||
exception = e;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
|
|
||||||
attachCookies(requestBuilder, cookieHeadersValue);
|
|
||||||
currentUrlRequest = requestBuilder.build();
|
|
||||||
currentUrlRequest.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
|
||||||
|
if (isEmpty(setCookieHeaders)) {
|
||||||
|
request.followRedirect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
urlRequest.cancel();
|
||||||
|
DataSpec redirectUrlDataSpec;
|
||||||
|
if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||||
|
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
||||||
|
// transformed into a GET.
|
||||||
|
redirectUrlDataSpec =
|
||||||
|
new DataSpec(
|
||||||
|
Uri.parse(newLocationUrl),
|
||||||
|
DataSpec.HTTP_METHOD_GET,
|
||||||
|
/* httpBody= */ null,
|
||||||
|
dataSpec.absoluteStreamPosition,
|
||||||
|
dataSpec.position,
|
||||||
|
dataSpec.length,
|
||||||
|
dataSpec.key,
|
||||||
|
dataSpec.flags);
|
||||||
|
} else {
|
||||||
|
redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
|
||||||
|
}
|
||||||
|
UrlRequest.Builder requestBuilder;
|
||||||
|
try {
|
||||||
|
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
|
||||||
|
} catch (IOException e) {
|
||||||
|
exception = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String cookieHeadersValue = parseCookies(setCookieHeaders);
|
||||||
|
attachCookies(requestBuilder, cookieHeadersValue);
|
||||||
|
currentUrlRequest = requestBuilder.build();
|
||||||
|
currentUrlRequest.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
|
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Predicate;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import org.chromium.net.CronetEngine;
|
import org.chromium.net.CronetEngine;
|
||||||
|
|
||||||
|
|
@ -45,8 +43,7 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
|
|
||||||
private final CronetEngineWrapper cronetEngineWrapper;
|
private final CronetEngineWrapper cronetEngineWrapper;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Predicate<String> contentTypePredicate;
|
@Nullable private final TransferListener transferListener;
|
||||||
private final @Nullable TransferListener transferListener;
|
|
||||||
private final int connectTimeoutMs;
|
private final int connectTimeoutMs;
|
||||||
private final int readTimeoutMs;
|
private final int readTimeoutMs;
|
||||||
private final boolean resetTimeoutOnRedirects;
|
private final boolean resetTimeoutOnRedirects;
|
||||||
|
|
@ -64,21 +61,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||||
* suitable CronetEngine can be build.
|
* suitable CronetEngine can be build.
|
||||||
*/
|
*/
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
HttpDataSource.Factory fallbackFactory) {
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
this(
|
this(
|
||||||
cronetEngineWrapper,
|
cronetEngineWrapper,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
/* transferListener= */ null,
|
/* transferListener= */ null,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
|
@ -98,20 +90,15 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
*/
|
*/
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
String userAgent) {
|
String userAgent) {
|
||||||
this(
|
this(
|
||||||
cronetEngineWrapper,
|
cronetEngineWrapper,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
/* transferListener= */ null,
|
/* transferListener= */ null,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
|
@ -132,9 +119,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
|
@ -143,7 +127,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
boolean resetTimeoutOnRedirects,
|
boolean resetTimeoutOnRedirects,
|
||||||
|
|
@ -151,7 +134,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
this(
|
this(
|
||||||
cronetEngineWrapper,
|
cronetEngineWrapper,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
/* transferListener= */ null,
|
/* transferListener= */ null,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
|
@ -172,9 +154,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||||
|
|
@ -184,7 +163,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
boolean resetTimeoutOnRedirects,
|
boolean resetTimeoutOnRedirects,
|
||||||
|
|
@ -192,7 +170,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
this(
|
this(
|
||||||
cronetEngineWrapper,
|
cronetEngineWrapper,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
/* transferListener= */ null,
|
/* transferListener= */ null,
|
||||||
connectTimeoutMs,
|
connectTimeoutMs,
|
||||||
readTimeoutMs,
|
readTimeoutMs,
|
||||||
|
|
@ -212,9 +189,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
* @param fallbackFactory A {@link HttpDataSource.Factory} which is used as a fallback in case no
|
||||||
* suitable CronetEngine can be build.
|
* suitable CronetEngine can be build.
|
||||||
|
|
@ -222,11 +196,16 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
@Nullable TransferListener transferListener,
|
@Nullable TransferListener transferListener,
|
||||||
HttpDataSource.Factory fallbackFactory) {
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
this(
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false, fallbackFactory);
|
cronetEngineWrapper,
|
||||||
|
executor,
|
||||||
|
transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
false,
|
||||||
|
fallbackFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -241,22 +220,27 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
* @param userAgent A user agent used to create a fallback HttpDataSource if needed.
|
||||||
*/
|
*/
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
@Nullable TransferListener transferListener,
|
@Nullable TransferListener transferListener,
|
||||||
String userAgent) {
|
String userAgent) {
|
||||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
this(
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false,
|
cronetEngineWrapper,
|
||||||
new DefaultHttpDataSourceFactory(userAgent, transferListener,
|
executor,
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, false));
|
transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
false,
|
||||||
|
new DefaultHttpDataSourceFactory(
|
||||||
|
userAgent,
|
||||||
|
transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -267,9 +251,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
|
@ -279,16 +260,20 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
@Nullable TransferListener transferListener,
|
@Nullable TransferListener transferListener,
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
boolean resetTimeoutOnRedirects,
|
boolean resetTimeoutOnRedirects,
|
||||||
String userAgent) {
|
String userAgent) {
|
||||||
this(cronetEngineWrapper, executor, contentTypePredicate, transferListener,
|
this(
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS, resetTimeoutOnRedirects,
|
cronetEngineWrapper,
|
||||||
new DefaultHttpDataSourceFactory(userAgent, transferListener, connectTimeoutMs,
|
executor,
|
||||||
readTimeoutMs, resetTimeoutOnRedirects));
|
transferListener,
|
||||||
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
new DefaultHttpDataSourceFactory(
|
||||||
|
userAgent, transferListener, connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -299,9 +284,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
*
|
*
|
||||||
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
* @param cronetEngineWrapper A {@link CronetEngineWrapper}.
|
||||||
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
* @param executor The {@link java.util.concurrent.Executor} that will perform the requests.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* CronetDataSource#open}.
|
|
||||||
* @param transferListener An optional listener.
|
* @param transferListener An optional listener.
|
||||||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||||
|
|
@ -312,7 +294,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
public CronetDataSourceFactory(
|
public CronetDataSourceFactory(
|
||||||
CronetEngineWrapper cronetEngineWrapper,
|
CronetEngineWrapper cronetEngineWrapper,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
@Nullable TransferListener transferListener,
|
@Nullable TransferListener transferListener,
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
|
|
@ -320,7 +301,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
HttpDataSource.Factory fallbackFactory) {
|
HttpDataSource.Factory fallbackFactory) {
|
||||||
this.cronetEngineWrapper = cronetEngineWrapper;
|
this.cronetEngineWrapper = cronetEngineWrapper;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.contentTypePredicate = contentTypePredicate;
|
|
||||||
this.transferListener = transferListener;
|
this.transferListener = transferListener;
|
||||||
this.connectTimeoutMs = connectTimeoutMs;
|
this.connectTimeoutMs = connectTimeoutMs;
|
||||||
this.readTimeoutMs = readTimeoutMs;
|
this.readTimeoutMs = readTimeoutMs;
|
||||||
|
|
@ -339,7 +319,6 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
||||||
new CronetDataSource(
|
new CronetDataSource(
|
||||||
cronetEngine,
|
cronetEngine,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
connectTimeoutMs,
|
connectTimeoutMs,
|
||||||
readTimeoutMs,
|
readTimeoutMs,
|
||||||
resetTimeoutOnRedirects,
|
resetTimeoutOnRedirects,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
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 {
|
||||||
*
|
*
|
||||||
* @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 {
|
||||||
*
|
*
|
||||||
* @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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Matchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
|
@ -38,7 +38,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.Predicate;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
|
|
@ -85,7 +84,6 @@ public final class CronetDataSourceTest {
|
||||||
|
|
||||||
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
|
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
|
||||||
@Mock private UrlRequest mockUrlRequest;
|
@Mock private UrlRequest mockUrlRequest;
|
||||||
@Mock private Predicate<String> mockContentTypePredicate;
|
|
||||||
@Mock private TransferListener mockTransferListener;
|
@Mock private TransferListener mockTransferListener;
|
||||||
@Mock private Executor mockExecutor;
|
@Mock private Executor mockExecutor;
|
||||||
@Mock private NetworkException mockNetworkException;
|
@Mock private NetworkException mockNetworkException;
|
||||||
|
|
@ -95,21 +93,19 @@ public final class CronetDataSourceTest {
|
||||||
private boolean redirectCalled;
|
private boolean redirectCalled;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
dataSourceUnderTest =
|
dataSourceUnderTest =
|
||||||
new CronetDataSource(
|
new CronetDataSource(
|
||||||
mockCronetEngine,
|
mockCronetEngine,
|
||||||
mockExecutor,
|
mockExecutor,
|
||||||
mockContentTypePredicate,
|
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
TEST_READ_TIMEOUT_MS,
|
TEST_READ_TIMEOUT_MS,
|
||||||
true, // resetTimeoutOnRedirects
|
/* resetTimeoutOnRedirects= */ true,
|
||||||
Clock.DEFAULT,
|
Clock.DEFAULT,
|
||||||
null,
|
/* defaultRequestProperties= */ null,
|
||||||
false);
|
/* handleSetCookieRequests= */ false);
|
||||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
|
||||||
when(mockCronetEngine.newUrlRequestBuilder(
|
when(mockCronetEngine.newUrlRequestBuilder(
|
||||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||||
.thenReturn(mockUrlRequestBuilder);
|
.thenReturn(mockUrlRequestBuilder);
|
||||||
|
|
@ -283,7 +279,13 @@ public final class CronetDataSourceTest {
|
||||||
@Test
|
@Test
|
||||||
public void testRequestOpenValidatesContentTypePredicate() {
|
public void testRequestOpenValidatesContentTypePredicate() {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(false);
|
|
||||||
|
ArrayList<String> testedContentTypes = new ArrayList<>();
|
||||||
|
dataSourceUnderTest.setContentTypePredicate(
|
||||||
|
(String input) -> {
|
||||||
|
testedContentTypes.add(input);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
@ -292,7 +294,8 @@ public final class CronetDataSourceTest {
|
||||||
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
|
assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
|
||||||
// Check for connection not automatically closed.
|
// Check for connection not automatically closed.
|
||||||
verify(mockUrlRequest, never()).cancel();
|
verify(mockUrlRequest, never()).cancel();
|
||||||
verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
|
assertThat(testedContentTypes).hasSize(1);
|
||||||
|
assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -551,6 +554,260 @@ public final class CronetDataSourceTest {
|
||||||
assertThat(bytesRead).isEqualTo(16);
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequestReadByteBufferTwice() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||||
|
|
||||||
|
// Use a wrapped ByteBuffer instead of direct for coverage.
|
||||||
|
returnedBuffer.rewind();
|
||||||
|
bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8));
|
||||||
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
|
|
||||||
|
// Separate cronet calls for each read.
|
||||||
|
verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
|
||||||
|
verify(mockTransferListener, times(2))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequestIntermixRead() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
// Chunking reads into parts 6, 7, 8, 9.
|
||||||
|
mockReadSuccess(0, 30);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6));
|
||||||
|
assertThat(bytesRead).isEqualTo(6);
|
||||||
|
|
||||||
|
byte[] returnedBytes = new byte[7];
|
||||||
|
bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7);
|
||||||
|
assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7));
|
||||||
|
assertThat(bytesRead).isEqualTo(6 + 7);
|
||||||
|
|
||||||
|
returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||||
|
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8));
|
||||||
|
assertThat(bytesRead).isEqualTo(6 + 7 + 8);
|
||||||
|
|
||||||
|
returnedBytes = new byte[9];
|
||||||
|
bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9);
|
||||||
|
assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9));
|
||||||
|
assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9);
|
||||||
|
|
||||||
|
// First ByteBuffer call. The first byte[] call populates enough bytes for the rest.
|
||||||
|
verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7);
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
testResponseHeader.put("Content-Length", Long.toString(1L));
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
// First request.
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||||
|
dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
dataSourceUnderTest.close();
|
||||||
|
|
||||||
|
testResponseHeader.remove("Content-Length");
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
// Second request.
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
|
returnedBuffer.limit(10);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(10);
|
||||||
|
returnedBuffer.limit(returnedBuffer.capacity());
|
||||||
|
bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(6);
|
||||||
|
returnedBuffer.rewind();
|
||||||
|
bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(1000, 5000);
|
||||||
|
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
||||||
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
|
||||||
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
|
||||||
|
// Tests for skipping bytes.
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 7000);
|
||||||
|
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||||
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
|
||||||
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadByteBufferWithUnsetLength() throws HttpDataSourceException {
|
||||||
|
testResponseHeader.remove("Content-Length");
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
|
returnedBuffer.limit(8);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||||
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadByteBufferReturnsWhatItCan() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16));
|
||||||
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOverreadByteBuffer() throws HttpDataSourceException {
|
||||||
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null);
|
||||||
|
testResponseHeader.put("Content-Length", Long.toString(16L));
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||||
|
|
||||||
|
// The current buffer is kept if not completely consumed by DataSource reader.
|
||||||
|
returnedBuffer = ByteBuffer.allocateDirect(6);
|
||||||
|
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(14);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6));
|
||||||
|
|
||||||
|
// 2 bytes left at this point.
|
||||||
|
returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||||
|
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2));
|
||||||
|
|
||||||
|
// Called on each.
|
||||||
|
verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
|
||||||
|
verify(mockTransferListener, times(1))
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);
|
||||||
|
|
||||||
|
// Now we already returned the 16 bytes initially asked.
|
||||||
|
// Try to read again even though all requested 16 bytes are already returned.
|
||||||
|
// Return C.RESULT_END_OF_INPUT
|
||||||
|
returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
|
int bytesOverRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
|
||||||
|
assertThat(returnedBuffer.position()).isEqualTo(0);
|
||||||
|
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
|
||||||
|
verify(mockTransferListener, never())
|
||||||
|
.onBytesTransferred(
|
||||||
|
dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
|
||||||
|
// Number of calls to cronet should not have increased.
|
||||||
|
verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
|
||||||
|
// Check for connection not automatically closed.
|
||||||
|
verify(mockUrlRequest, never()).cancel();
|
||||||
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClosedMeansClosedReadByteBuffer() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, 16);
|
||||||
|
|
||||||
|
int bytesRead = 0;
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
|
returnedBuffer.limit(8);
|
||||||
|
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
returnedBuffer.flip();
|
||||||
|
assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
|
||||||
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
|
|
||||||
|
dataSourceUnderTest.close();
|
||||||
|
verify(mockTransferListener)
|
||||||
|
.onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
bytesRead += dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
fail();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// Expected.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16 bytes were attempted but only 8 should have been successfully read.
|
||||||
|
assertThat(bytesRead).isEqualTo(8);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testConnectTimeout() throws InterruptedException {
|
public void testConnectTimeout() throws InterruptedException {
|
||||||
long startTimeMs = SystemClock.elapsedRealtime();
|
long startTimeMs = SystemClock.elapsedRealtime();
|
||||||
|
|
@ -734,7 +991,6 @@ public final class CronetDataSourceTest {
|
||||||
new CronetDataSource(
|
new CronetDataSource(
|
||||||
mockCronetEngine,
|
mockCronetEngine,
|
||||||
mockExecutor,
|
mockExecutor,
|
||||||
mockContentTypePredicate,
|
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
TEST_READ_TIMEOUT_MS,
|
TEST_READ_TIMEOUT_MS,
|
||||||
true, // resetTimeoutOnRedirects
|
true, // resetTimeoutOnRedirects
|
||||||
|
|
@ -765,13 +1021,12 @@ public final class CronetDataSourceTest {
|
||||||
new CronetDataSource(
|
new CronetDataSource(
|
||||||
mockCronetEngine,
|
mockCronetEngine,
|
||||||
mockExecutor,
|
mockExecutor,
|
||||||
mockContentTypePredicate,
|
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
TEST_READ_TIMEOUT_MS,
|
TEST_READ_TIMEOUT_MS,
|
||||||
true, // resetTimeoutOnRedirects
|
/* resetTimeoutOnRedirects= */ true,
|
||||||
Clock.DEFAULT,
|
Clock.DEFAULT,
|
||||||
null,
|
/* defaultRequestProperties= */ null,
|
||||||
true);
|
/* handleSetCookieRequests= */ true);
|
||||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||||
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
|
||||||
|
|
||||||
|
|
@ -804,13 +1059,12 @@ public final class CronetDataSourceTest {
|
||||||
new CronetDataSource(
|
new CronetDataSource(
|
||||||
mockCronetEngine,
|
mockCronetEngine,
|
||||||
mockExecutor,
|
mockExecutor,
|
||||||
mockContentTypePredicate,
|
|
||||||
TEST_CONNECT_TIMEOUT_MS,
|
TEST_CONNECT_TIMEOUT_MS,
|
||||||
TEST_READ_TIMEOUT_MS,
|
TEST_READ_TIMEOUT_MS,
|
||||||
true, // resetTimeoutOnRedirects
|
/* resetTimeoutOnRedirects= */ true,
|
||||||
Clock.DEFAULT,
|
Clock.DEFAULT,
|
||||||
null,
|
/* defaultRequestProperties= */ null,
|
||||||
true);
|
/* handleSetCookieRequests= */ true);
|
||||||
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
dataSourceUnderTest.addTransferListener(mockTransferListener);
|
||||||
mockSingleRedirectSuccess();
|
mockSingleRedirectSuccess();
|
||||||
mockFollowRedirectSuccess();
|
mockFollowRedirectSuccess();
|
||||||
|
|
@ -855,6 +1109,36 @@ public final class CronetDataSourceTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadByteBufferFailure() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadFailure();
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||||
|
try {
|
||||||
|
dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
fail("dataSourceUnderTest.read() returned, but IOException expected");
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Expected.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadNonDirectedByteBufferFailure() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadFailure();
|
||||||
|
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
byte[] returnedBuffer = new byte[8];
|
||||||
|
try {
|
||||||
|
dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer));
|
||||||
|
fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// Expected.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
|
public void testReadInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||||
mockResponseStartSuccess();
|
mockResponseStartSuccess();
|
||||||
|
|
@ -886,6 +1170,37 @@ public final class CronetDataSourceTest {
|
||||||
timedOutLatch.await();
|
timedOutLatch.await();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
dataSourceUnderTest.open(testDataSpec);
|
||||||
|
|
||||||
|
final ConditionVariable startCondition = buildReadStartedCondition();
|
||||||
|
final CountDownLatch timedOutLatch = new CountDownLatch(1);
|
||||||
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
|
||||||
|
Thread thread =
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
fail();
|
||||||
|
} catch (HttpDataSourceException e) {
|
||||||
|
// Expected.
|
||||||
|
assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
|
||||||
|
timedOutLatch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.start();
|
||||||
|
startCondition.block();
|
||||||
|
|
||||||
|
assertNotCountedDown(timedOutLatch);
|
||||||
|
// Now we interrupt.
|
||||||
|
thread.interrupt();
|
||||||
|
timedOutLatch.await();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAllowDirectExecutor() throws HttpDataSourceException {
|
public void testAllowDirectExecutor() throws HttpDataSourceException {
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
|
||||||
|
|
@ -1064,4 +1379,17 @@ public final class CronetDataSourceTest {
|
||||||
testBuffer.flip();
|
testBuffer.flip();
|
||||||
return testBuffer;
|
return testBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a copy of what is remaining in the src buffer from the current position to capacity.
|
||||||
|
private static byte[] copyByteBufferToArray(ByteBuffer src) {
|
||||||
|
if (src == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] copy = new byte[src.remaining()];
|
||||||
|
int index = 0;
|
||||||
|
while (src.hasRemaining()) {
|
||||||
|
copy[index++] = src.get();
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
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;
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
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 {
|
||||||
|
|
||||||
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 @@
|
||||||
-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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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 {
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
* 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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
if (tempBuffer == null) {
|
|
||||||
this.tempBuffer = new byte[TEMP_BUFFER_SIZE];
|
|
||||||
}
|
|
||||||
endOfExtractorInput = false;
|
endOfExtractorInput = false;
|
||||||
|
if (tempBuffer == null) {
|
||||||
|
tempBuffer = new byte[TEMP_BUFFER_SIZE];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.extractor.BinarySearchSeeker;
|
import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder;
|
||||||
import com.google.android.exoplayer2.extractor.Extractor;
|
import com.google.android.exoplayer2.extractor.Extractor;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
|
|
@ -33,7 +33,8 @@ import com.google.android.exoplayer2.extractor.SeekPoint;
|
||||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer2.metadata.Metadata;
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||||
import com.google.android.exoplayer2.util.FlacStreamInfo;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.FlacStreamMetadata;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -42,6 +43,9 @@ import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Facilitates the extraction of data from the FLAC container format.
|
* Facilitates the extraction of data from the FLAC container format.
|
||||||
|
|
@ -74,23 +78,20 @@ public final class FlacExtractor implements Extractor {
|
||||||
*/
|
*/
|
||||||
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
|
private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22};
|
||||||
|
|
||||||
|
private final ParsableByteArray outputBuffer;
|
||||||
private final Id3Peeker id3Peeker;
|
private final Id3Peeker id3Peeker;
|
||||||
private final boolean isId3MetadataDisabled;
|
private final boolean id3MetadataDisabled;
|
||||||
|
|
||||||
private FlacDecoderJni decoderJni;
|
@Nullable private FlacDecoderJni decoderJni;
|
||||||
|
private @MonotonicNonNull ExtractorOutput extractorOutput;
|
||||||
|
private @MonotonicNonNull TrackOutput trackOutput;
|
||||||
|
|
||||||
private ExtractorOutput extractorOutput;
|
private boolean streamMetadataDecoded;
|
||||||
private TrackOutput trackOutput;
|
private @MonotonicNonNull FlacStreamMetadata streamMetadata;
|
||||||
|
private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
|
||||||
|
|
||||||
private ParsableByteArray outputBuffer;
|
@Nullable private Metadata id3Metadata;
|
||||||
private ByteBuffer outputByteBuffer;
|
@Nullable private FlacBinarySearchSeeker binarySearchSeeker;
|
||||||
private BinarySearchSeeker.OutputFrameHolder outputFrameHolder;
|
|
||||||
private FlacStreamInfo streamInfo;
|
|
||||||
|
|
||||||
private Metadata id3Metadata;
|
|
||||||
private @Nullable FlacBinarySearchSeeker flacBinarySearchSeeker;
|
|
||||||
|
|
||||||
private boolean readPastStreamInfo;
|
|
||||||
|
|
||||||
/** Constructs an instance with flags = 0. */
|
/** Constructs an instance with flags = 0. */
|
||||||
public FlacExtractor() {
|
public FlacExtractor() {
|
||||||
|
|
@ -103,8 +104,9 @@ public final class FlacExtractor implements Extractor {
|
||||||
* @param flags Flags that control the extractor's behavior.
|
* @param flags Flags that control the extractor's behavior.
|
||||||
*/
|
*/
|
||||||
public FlacExtractor(int flags) {
|
public FlacExtractor(int flags) {
|
||||||
|
outputBuffer = new ParsableByteArray();
|
||||||
id3Peeker = new Id3Peeker();
|
id3Peeker = new Id3Peeker();
|
||||||
isId3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
|
id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -130,48 +132,53 @@ public final class FlacExtractor implements Extractor {
|
||||||
@Override
|
@Override
|
||||||
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
public int read(final ExtractorInput input, PositionHolder seekPosition)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
if (input.getPosition() == 0 && !isId3MetadataDisabled && id3Metadata == null) {
|
if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
|
||||||
id3Metadata = peekId3Data(input);
|
id3Metadata = peekId3Data(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
decoderJni.setData(input);
|
FlacDecoderJni decoderJni = initDecoderJni(input);
|
||||||
readPastStreamInfo(input);
|
|
||||||
|
|
||||||
if (flacBinarySearchSeeker != null && flacBinarySearchSeeker.isSeeking()) {
|
|
||||||
return handlePendingSeek(input, seekPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
long lastDecodePosition = decoderJni.getDecodePosition();
|
|
||||||
try {
|
try {
|
||||||
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
|
decodeStreamMetadata(input);
|
||||||
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
|
||||||
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
|
|
||||||
}
|
|
||||||
int outputSize = outputByteBuffer.limit();
|
|
||||||
if (outputSize == 0) {
|
|
||||||
return RESULT_END_OF_INPUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeLastSampleToOutput(outputSize, decoderJni.getLastFrameTimestamp());
|
if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
|
||||||
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||||
|
long lastDecodePosition = decoderJni.getDecodePosition();
|
||||||
|
try {
|
||||||
|
decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
|
||||||
|
} catch (FlacDecoderJni.FlacFrameDecodeException e) {
|
||||||
|
throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
|
||||||
|
}
|
||||||
|
int outputSize = outputByteBuffer.limit();
|
||||||
|
if (outputSize == 0) {
|
||||||
|
return RESULT_END_OF_INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
|
||||||
|
return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
|
||||||
|
} finally {
|
||||||
|
decoderJni.clearData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek(long position, long timeUs) {
|
public void seek(long position, long timeUs) {
|
||||||
if (position == 0) {
|
if (position == 0) {
|
||||||
readPastStreamInfo = false;
|
streamMetadataDecoded = false;
|
||||||
}
|
}
|
||||||
if (decoderJni != null) {
|
if (decoderJni != null) {
|
||||||
decoderJni.reset(position);
|
decoderJni.reset(position);
|
||||||
}
|
}
|
||||||
if (flacBinarySearchSeeker != null) {
|
if (binarySearchSeeker != null) {
|
||||||
flacBinarySearchSeeker.setSeekTargetUs(timeUs);
|
binarySearchSeeker.setSeekTargetUs(timeUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
flacBinarySearchSeeker = null;
|
binarySearchSeeker = null;
|
||||||
if (decoderJni != null) {
|
if (decoderJni != null) {
|
||||||
decoderJni.release();
|
decoderJni.release();
|
||||||
decoderJni = null;
|
decoderJni = null;
|
||||||
|
|
@ -179,123 +186,141 @@ public final class FlacExtractor implements Extractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Peeks ID3 tag data (if present) at the beginning of the input.
|
* Peeks ID3 tag data at the beginning of the input.
|
||||||
*
|
*
|
||||||
* @return The first ID3 tag decoded into a {@link Metadata} object. May be null if ID3 tag is not
|
* @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input.
|
||||||
* present in the input.
|
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
|
private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
|
||||||
input.resetPeekPosition();
|
input.resetPeekPosition();
|
||||||
Id3Decoder.FramePredicate id3FramePredicate =
|
Id3Decoder.FramePredicate id3FramePredicate =
|
||||||
isId3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
|
id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null;
|
||||||
return id3Peeker.peekId3Data(input, id3FramePredicate);
|
return id3Peeker.peekId3Data(input, id3FramePredicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
|
||||||
|
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
|
||||||
|
private FlacDecoderJni initDecoderJni(ExtractorInput input) {
|
||||||
|
FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
|
||||||
|
decoderJni.setData(input);
|
||||||
|
return decoderJni;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
|
||||||
|
@EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
|
||||||
|
@SuppressWarnings({"contracts.postcondition.not.satisfied"})
|
||||||
|
private void decodeStreamMetadata(ExtractorInput input) throws InterruptedException, IOException {
|
||||||
|
if (streamMetadataDecoded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlacStreamMetadata streamMetadata;
|
||||||
|
try {
|
||||||
|
streamMetadata = decoderJni.decodeStreamMetadata();
|
||||||
|
} catch (IOException e) {
|
||||||
|
decoderJni.reset(/* newPosition= */ 0);
|
||||||
|
input.setRetryPosition(/* position= */ 0, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamMetadataDecoded = true;
|
||||||
|
if (this.streamMetadata == null) {
|
||||||
|
this.streamMetadata = streamMetadata;
|
||||||
|
binarySearchSeeker =
|
||||||
|
outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput);
|
||||||
|
Metadata metadata = id3MetadataDisabled ? null : id3Metadata;
|
||||||
|
if (streamMetadata.metadata != null) {
|
||||||
|
metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata);
|
||||||
|
}
|
||||||
|
outputFormat(streamMetadata, metadata, trackOutput);
|
||||||
|
outputBuffer.reset(streamMetadata.maxDecodedFrameSize());
|
||||||
|
outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresNonNull("binarySearchSeeker")
|
||||||
|
private int handlePendingSeek(
|
||||||
|
ExtractorInput input,
|
||||||
|
PositionHolder seekPosition,
|
||||||
|
ParsableByteArray outputBuffer,
|
||||||
|
OutputFrameHolder outputFrameHolder,
|
||||||
|
TrackOutput trackOutput)
|
||||||
|
throws InterruptedException, IOException {
|
||||||
|
int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
|
||||||
|
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
||||||
|
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
||||||
|
outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
|
||||||
|
}
|
||||||
|
return seekResult;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
|
* Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present.
|
||||||
*
|
*
|
||||||
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
|
* @return Whether the input begins with {@link #FLAC_SIGNATURE}.
|
||||||
*/
|
*/
|
||||||
private boolean peekFlacSignature(ExtractorInput input) throws IOException, InterruptedException {
|
private static boolean peekFlacSignature(ExtractorInput input)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
byte[] header = new byte[FLAC_SIGNATURE.length];
|
byte[] header = new byte[FLAC_SIGNATURE.length];
|
||||||
input.peekFully(header, 0, FLAC_SIGNATURE.length);
|
input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length);
|
||||||
return Arrays.equals(header, FLAC_SIGNATURE);
|
return Arrays.equals(header, FLAC_SIGNATURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readPastStreamInfo(ExtractorInput input) throws InterruptedException, IOException {
|
/**
|
||||||
if (readPastStreamInfo) {
|
* Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
|
||||||
return;
|
* handle seeks.
|
||||||
}
|
*/
|
||||||
|
@Nullable
|
||||||
FlacStreamInfo streamInfo = decodeStreamInfo(input);
|
private static FlacBinarySearchSeeker outputSeekMap(
|
||||||
readPastStreamInfo = true;
|
FlacDecoderJni decoderJni,
|
||||||
if (this.streamInfo == null) {
|
FlacStreamMetadata streamMetadata,
|
||||||
updateFlacStreamInfo(input, streamInfo);
|
long streamLength,
|
||||||
}
|
ExtractorOutput output) {
|
||||||
}
|
boolean hasSeekTable = decoderJni.getSeekPosition(/* timeUs= */ 0) != -1;
|
||||||
|
FlacBinarySearchSeeker binarySearchSeeker = null;
|
||||||
private void updateFlacStreamInfo(ExtractorInput input, FlacStreamInfo streamInfo) {
|
SeekMap seekMap;
|
||||||
this.streamInfo = streamInfo;
|
if (hasSeekTable) {
|
||||||
outputSeekMap(input, streamInfo);
|
seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni);
|
||||||
outputFormat(streamInfo);
|
} else if (streamLength != C.LENGTH_UNSET) {
|
||||||
outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize());
|
|
||||||
outputByteBuffer = ByteBuffer.wrap(outputBuffer.data);
|
|
||||||
outputFrameHolder = new BinarySearchSeeker.OutputFrameHolder(outputByteBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private FlacStreamInfo decodeStreamInfo(ExtractorInput input)
|
|
||||||
throws InterruptedException, IOException {
|
|
||||||
try {
|
|
||||||
FlacStreamInfo streamInfo = decoderJni.decodeMetadata();
|
|
||||||
if (streamInfo == null) {
|
|
||||||
throw new IOException("Metadata decoding failed");
|
|
||||||
}
|
|
||||||
return streamInfo;
|
|
||||||
} catch (IOException e) {
|
|
||||||
decoderJni.reset(0);
|
|
||||||
input.setRetryPosition(0, e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void outputSeekMap(ExtractorInput input, FlacStreamInfo streamInfo) {
|
|
||||||
boolean hasSeekTable = decoderJni.getSeekPosition(0) != -1;
|
|
||||||
SeekMap seekMap =
|
|
||||||
hasSeekTable
|
|
||||||
? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
|
|
||||||
: getSeekMapForNonSeekTableFlac(input, streamInfo);
|
|
||||||
extractorOutput.seekMap(seekMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SeekMap getSeekMapForNonSeekTableFlac(ExtractorInput input, FlacStreamInfo streamInfo) {
|
|
||||||
long inputLength = input.getLength();
|
|
||||||
if (inputLength != C.LENGTH_UNSET) {
|
|
||||||
long firstFramePosition = decoderJni.getDecodePosition();
|
long firstFramePosition = decoderJni.getDecodePosition();
|
||||||
flacBinarySearchSeeker =
|
binarySearchSeeker =
|
||||||
new FlacBinarySearchSeeker(streamInfo, firstFramePosition, inputLength, decoderJni);
|
new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni);
|
||||||
return flacBinarySearchSeeker.getSeekMap();
|
seekMap = binarySearchSeeker.getSeekMap();
|
||||||
} else { // can't seek at all, because there's no SeekTable and the input length is unknown.
|
} else {
|
||||||
return new SeekMap.Unseekable(streamInfo.durationUs());
|
seekMap = new SeekMap.Unseekable(streamMetadata.durationUs());
|
||||||
}
|
}
|
||||||
|
output.seekMap(seekMap);
|
||||||
|
return binarySearchSeeker;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void outputFormat(FlacStreamInfo streamInfo) {
|
private static void outputFormat(
|
||||||
|
FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
|
||||||
Format mediaFormat =
|
Format mediaFormat =
|
||||||
Format.createAudioSampleFormat(
|
Format.createAudioSampleFormat(
|
||||||
/* id= */ null,
|
/* id= */ null,
|
||||||
MimeTypes.AUDIO_RAW,
|
MimeTypes.AUDIO_RAW,
|
||||||
/* codecs= */ null,
|
/* codecs= */ null,
|
||||||
streamInfo.bitRate(),
|
streamMetadata.bitRate(),
|
||||||
streamInfo.maxDecodedFrameSize(),
|
streamMetadata.maxDecodedFrameSize(),
|
||||||
streamInfo.channels,
|
streamMetadata.channels,
|
||||||
streamInfo.sampleRate,
|
streamMetadata.sampleRate,
|
||||||
getPcmEncoding(streamInfo.bitsPerSample),
|
getPcmEncoding(streamMetadata.bitsPerSample),
|
||||||
/* encoderDelay= */ 0,
|
/* encoderDelay= */ 0,
|
||||||
/* encoderPadding= */ 0,
|
/* encoderPadding= */ 0,
|
||||||
/* initializationData= */ null,
|
/* initializationData= */ null,
|
||||||
/* drmInitData= */ null,
|
/* drmInitData= */ null,
|
||||||
/* selectionFlags= */ 0,
|
/* selectionFlags= */ 0,
|
||||||
/* language= */ null,
|
/* language= */ null,
|
||||||
isId3MetadataDisabled ? null : id3Metadata);
|
metadata);
|
||||||
trackOutput.format(mediaFormat);
|
output.format(mediaFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int handlePendingSeek(ExtractorInput input, PositionHolder seekPosition)
|
private static void outputSample(
|
||||||
throws InterruptedException, IOException {
|
ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
|
||||||
int seekResult =
|
sampleData.setPosition(0);
|
||||||
flacBinarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder);
|
output.sampleData(sampleData, size);
|
||||||
ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
|
output.sampleMetadata(
|
||||||
if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
|
timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
|
||||||
writeLastSampleToOutput(outputByteBuffer.limit(), outputFrameHolder.timeUs);
|
|
||||||
}
|
|
||||||
return seekResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeLastSampleToOutput(int size, long lastSampleTimestamp) {
|
|
||||||
outputBuffer.setPosition(0);
|
|
||||||
trackOutput.sampleData(outputBuffer, size);
|
|
||||||
trackOutput.sampleMetadata(lastSampleTimestamp, C.BUFFER_FLAG_KEY_FRAME, size, 0, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
|
/** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
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 {
|
||||||
* @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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
* 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) {
|
||||||
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>",
|
||||||
|
"(IIIIIIIJLjava/util/List;Ljava/util/List;)V");
|
||||||
|
|
||||||
return env->NewObject(cls, constructor, streamInfo.min_blocksize,
|
return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor,
|
||||||
streamInfo.max_blocksize, streamInfo.min_framesize,
|
streamInfo.min_blocksize, streamInfo.max_blocksize,
|
||||||
streamInfo.max_framesize, streamInfo.sample_rate,
|
streamInfo.min_framesize, streamInfo.max_framesize,
|
||||||
streamInfo.channels, streamInfo.bits_per_sample,
|
streamInfo.sample_rate, streamInfo.channels,
|
||||||
streamInfo.total_samples);
|
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) {
|
||||||
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)
|
||||||
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() {
|
||||||
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 @@
|
||||||
|
|
||||||
#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 @@
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 @@
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
*/
|
*/
|
||||||
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
|
||||||
* @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
|
||||||
// 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
|
||||||
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
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
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;
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Ad getAd() {
|
@Nullable
|
||||||
|
public Ad getAd() {
|
||||||
return ad;
|
return ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
# 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
|
||||||
[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;
|
||||||
*
|
*
|
||||||
* @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;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects a {@link MediaSessionCompat} to a {@link Player}.
|
* Connects a {@link MediaSessionCompat} to a {@link Player}.
|
||||||
|
|
@ -172,7 +173,7 @@ public final class MediaSessionConnector {
|
||||||
ResultReceiver cb);
|
ResultReceiver cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interface to which playback preparation actions are delegated. */
|
/** Interface to which playback preparation and play actions are delegated. */
|
||||||
public interface PlaybackPreparer extends CommandReceiver {
|
public interface PlaybackPreparer extends CommandReceiver {
|
||||||
|
|
||||||
long ACTIONS =
|
long ACTIONS =
|
||||||
|
|
@ -197,14 +198,36 @@ public final class MediaSessionConnector {
|
||||||
* @return The bitmask of the supported media actions.
|
* @return The bitmask of the supported media actions.
|
||||||
*/
|
*/
|
||||||
long getSupportedPrepareActions();
|
long getSupportedPrepareActions();
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepare()}. */
|
/**
|
||||||
void onPrepare();
|
* See {@link MediaSessionCompat.Callback#onPrepare()}.
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */
|
*
|
||||||
void onPrepareFromMediaId(String mediaId, Bundle extras);
|
* @param playWhenReady Whether playback should be started after preparation.
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */
|
*/
|
||||||
void onPrepareFromSearch(String query, Bundle extras);
|
void onPrepare(boolean playWhenReady);
|
||||||
/** See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */
|
/**
|
||||||
void onPrepareFromUri(Uri uri, Bundle extras);
|
* See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}.
|
||||||
|
*
|
||||||
|
* @param mediaId The media id of the media item to be prepared.
|
||||||
|
* @param playWhenReady Whether playback should be started after preparation.
|
||||||
|
* @param extras A {@link Bundle} of extras passed by the media controller.
|
||||||
|
*/
|
||||||
|
void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
|
||||||
|
/**
|
||||||
|
* See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
|
||||||
|
*
|
||||||
|
* @param query The search query.
|
||||||
|
* @param playWhenReady Whether playback should be started after preparation.
|
||||||
|
* @param extras A {@link Bundle} of extras passed by the media controller.
|
||||||
|
*/
|
||||||
|
void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
|
||||||
|
/**
|
||||||
|
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
|
||||||
|
*
|
||||||
|
* @param uri The {@link Uri} of the media item to be prepared.
|
||||||
|
* @param playWhenReady Whether playback should be started after preparation.
|
||||||
|
* @param extras A {@link Bundle} of extras passed by the media controller.
|
||||||
|
*/
|
||||||
|
void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -337,7 +360,7 @@ public final class MediaSessionConnector {
|
||||||
* @param extras Optional extras sent by a media controller.
|
* @param extras Optional extras sent by a media controller.
|
||||||
*/
|
*/
|
||||||
void onCustomAction(
|
void onCustomAction(
|
||||||
Player player, ControlDispatcher controlDispatcher, String action, Bundle extras);
|
Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
|
* Returns a {@link PlaybackStateCompat.CustomAction} which will be published to the media
|
||||||
|
|
@ -355,6 +378,13 @@ public final class MediaSessionConnector {
|
||||||
/**
|
/**
|
||||||
* Gets the {@link MediaMetadataCompat} to be published to the session.
|
* Gets the {@link MediaMetadataCompat} to be published to the session.
|
||||||
*
|
*
|
||||||
|
* <p>An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
|
||||||
|
* case the app should return a {@link MediaMetadataCompat} object that does not contain these
|
||||||
|
* resources as a placeholder. The app should start an asynchronous operation to download the
|
||||||
|
* bitmap and put it into a cache. Finally, the app should call {@link
|
||||||
|
* #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
|
||||||
|
* can now return a {@link MediaMetadataCompat} object with all the resources included.
|
||||||
|
*
|
||||||
* @param player The player connected to the media session.
|
* @param player The player connected to the media session.
|
||||||
* @return The {@link MediaMetadataCompat} to be published to the session.
|
* @return The {@link MediaMetadataCompat} to be published to the session.
|
||||||
*/
|
*/
|
||||||
|
|
@ -528,7 +558,7 @@ public final class MediaSessionConnector {
|
||||||
*
|
*
|
||||||
* @param queueNavigator The queue navigator.
|
* @param queueNavigator The queue navigator.
|
||||||
*/
|
*/
|
||||||
public void setQueueNavigator(QueueNavigator queueNavigator) {
|
public void setQueueNavigator(@Nullable QueueNavigator queueNavigator) {
|
||||||
if (this.queueNavigator != queueNavigator) {
|
if (this.queueNavigator != queueNavigator) {
|
||||||
unregisterCommandReceiver(this.queueNavigator);
|
unregisterCommandReceiver(this.queueNavigator);
|
||||||
this.queueNavigator = queueNavigator;
|
this.queueNavigator = queueNavigator;
|
||||||
|
|
@ -541,7 +571,7 @@ public final class MediaSessionConnector {
|
||||||
*
|
*
|
||||||
* @param queueEditor The queue editor.
|
* @param queueEditor The queue editor.
|
||||||
*/
|
*/
|
||||||
public void setQueueEditor(QueueEditor queueEditor) {
|
public void setQueueEditor(@Nullable QueueEditor queueEditor) {
|
||||||
if (this.queueEditor != queueEditor) {
|
if (this.queueEditor != queueEditor) {
|
||||||
unregisterCommandReceiver(this.queueEditor);
|
unregisterCommandReceiver(this.queueEditor);
|
||||||
this.queueEditor = queueEditor;
|
this.queueEditor = queueEditor;
|
||||||
|
|
@ -643,7 +673,7 @@ public final class MediaSessionConnector {
|
||||||
mediaMetadataProvider != null && player != null
|
mediaMetadataProvider != null && player != null
|
||||||
? mediaMetadataProvider.getMetadata(player)
|
? mediaMetadataProvider.getMetadata(player)
|
||||||
: METADATA_EMPTY;
|
: METADATA_EMPTY;
|
||||||
mediaSession.setMetadata(metadata != null ? metadata : METADATA_EMPTY);
|
mediaSession.setMetadata(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -654,6 +684,7 @@ public final class MediaSessionConnector {
|
||||||
*/
|
*/
|
||||||
public final void invalidateMediaSessionPlaybackState() {
|
public final void invalidateMediaSessionPlaybackState() {
|
||||||
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
|
||||||
|
@Nullable Player player = this.player;
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
|
builder.setActions(buildPrepareActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0);
|
||||||
mediaSession.setPlaybackState(builder.build());
|
mediaSession.setPlaybackState(builder.build());
|
||||||
|
|
@ -662,6 +693,7 @@ public final class MediaSessionConnector {
|
||||||
|
|
||||||
Map<String, CustomActionProvider> currentActions = new HashMap<>();
|
Map<String, CustomActionProvider> currentActions = new HashMap<>();
|
||||||
for (CustomActionProvider customActionProvider : customActionProviders) {
|
for (CustomActionProvider customActionProvider : customActionProviders) {
|
||||||
|
@Nullable
|
||||||
PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
|
PlaybackStateCompat.CustomAction customAction = customActionProvider.getCustomAction(player);
|
||||||
if (customAction != null) {
|
if (customAction != null) {
|
||||||
currentActions.put(customAction.getAction(), customActionProvider);
|
currentActions.put(customAction.getAction(), customActionProvider);
|
||||||
|
|
@ -672,6 +704,7 @@ public final class MediaSessionConnector {
|
||||||
|
|
||||||
int playbackState = player.getPlaybackState();
|
int playbackState = player.getPlaybackState();
|
||||||
Bundle extras = new Bundle();
|
Bundle extras = new Bundle();
|
||||||
|
@Nullable
|
||||||
ExoPlaybackException playbackError =
|
ExoPlaybackException playbackError =
|
||||||
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
|
playbackState == Player.STATE_IDLE ? player.getPlaybackError() : null;
|
||||||
boolean reportError = playbackError != null || customError != null;
|
boolean reportError = playbackError != null || customError != null;
|
||||||
|
|
@ -727,8 +760,8 @@ public final class MediaSessionConnector {
|
||||||
*
|
*
|
||||||
* @param commandReceiver The command receiver to register.
|
* @param commandReceiver The command receiver to register.
|
||||||
*/
|
*/
|
||||||
public void registerCustomCommandReceiver(CommandReceiver commandReceiver) {
|
public void registerCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
|
||||||
if (!customCommandReceivers.contains(commandReceiver)) {
|
if (commandReceiver != null && !customCommandReceivers.contains(commandReceiver)) {
|
||||||
customCommandReceivers.add(commandReceiver);
|
customCommandReceivers.add(commandReceiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -738,18 +771,22 @@ public final class MediaSessionConnector {
|
||||||
*
|
*
|
||||||
* @param commandReceiver The command receiver to unregister.
|
* @param commandReceiver The command receiver to unregister.
|
||||||
*/
|
*/
|
||||||
public void unregisterCustomCommandReceiver(CommandReceiver commandReceiver) {
|
public void unregisterCustomCommandReceiver(@Nullable CommandReceiver commandReceiver) {
|
||||||
customCommandReceivers.remove(commandReceiver);
|
if (commandReceiver != null) {
|
||||||
|
customCommandReceivers.remove(commandReceiver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerCommandReceiver(CommandReceiver commandReceiver) {
|
private void registerCommandReceiver(@Nullable CommandReceiver commandReceiver) {
|
||||||
if (!commandReceivers.contains(commandReceiver)) {
|
if (commandReceiver != null && !commandReceivers.contains(commandReceiver)) {
|
||||||
commandReceivers.add(commandReceiver);
|
commandReceivers.add(commandReceiver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterCommandReceiver(CommandReceiver commandReceiver) {
|
private void unregisterCommandReceiver(@Nullable CommandReceiver commandReceiver) {
|
||||||
commandReceivers.remove(commandReceiver);
|
if (commandReceiver != null) {
|
||||||
|
commandReceivers.remove(commandReceiver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private long buildPrepareActions() {
|
private long buildPrepareActions() {
|
||||||
|
|
@ -807,39 +844,47 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(result = true, expression = "player")
|
||||||
private boolean canDispatchPlaybackAction(long action) {
|
private boolean canDispatchPlaybackAction(long action) {
|
||||||
return player != null && (enabledPlaybackActions & action) != 0;
|
return player != null && (enabledPlaybackActions & action) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(result = true, expression = "playbackPreparer")
|
||||||
private boolean canDispatchToPlaybackPreparer(long action) {
|
private boolean canDispatchToPlaybackPreparer(long action) {
|
||||||
return playbackPreparer != null
|
return playbackPreparer != null
|
||||||
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
|
&& (playbackPreparer.getSupportedPrepareActions() & action) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(
|
||||||
|
result = true,
|
||||||
|
expression = {"player", "queueNavigator"})
|
||||||
private boolean canDispatchToQueueNavigator(long action) {
|
private boolean canDispatchToQueueNavigator(long action) {
|
||||||
return player != null
|
return player != null
|
||||||
&& queueNavigator != null
|
&& queueNavigator != null
|
||||||
&& (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
|
&& (queueNavigator.getSupportedQueueNavigatorActions(player) & action) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(
|
||||||
|
result = true,
|
||||||
|
expression = {"player", "ratingCallback"})
|
||||||
private boolean canDispatchSetRating() {
|
private boolean canDispatchSetRating() {
|
||||||
return player != null && ratingCallback != null;
|
return player != null && ratingCallback != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(
|
||||||
|
result = true,
|
||||||
|
expression = {"player", "queueEditor"})
|
||||||
private boolean canDispatchQueueEdit() {
|
private boolean canDispatchQueueEdit() {
|
||||||
return player != null && queueEditor != null;
|
return player != null && queueEditor != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EnsuresNonNullIf(
|
||||||
|
result = true,
|
||||||
|
expression = {"player", "mediaButtonEventHandler"})
|
||||||
private boolean canDispatchMediaButtonEvent() {
|
private boolean canDispatchMediaButtonEvent() {
|
||||||
return player != null && mediaButtonEventHandler != null;
|
return player != null && mediaButtonEventHandler != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPlayWhenReady(boolean playWhenReady) {
|
|
||||||
if (player != null) {
|
|
||||||
controlDispatcher.dispatchSetPlayWhenReady(player, playWhenReady);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rewind(Player player) {
|
private void rewind(Player player) {
|
||||||
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
|
if (player.isCurrentWindowSeekable() && rewindMs > 0) {
|
||||||
seekTo(player, player.getCurrentPosition() - rewindMs);
|
seekTo(player, player.getCurrentPosition() - rewindMs);
|
||||||
|
|
@ -906,10 +951,10 @@ public final class MediaSessionConnector {
|
||||||
MediaSessionCompat.QueueItem queueItem = queue.get(i);
|
MediaSessionCompat.QueueItem queueItem = queue.get(i);
|
||||||
if (queueItem.getQueueId() == activeQueueItemId) {
|
if (queueItem.getQueueId() == activeQueueItemId) {
|
||||||
MediaDescriptionCompat description = queueItem.getDescription();
|
MediaDescriptionCompat description = queueItem.getDescription();
|
||||||
Bundle extras = description.getExtras();
|
@Nullable Bundle extras = description.getExtras();
|
||||||
if (extras != null) {
|
if (extras != null) {
|
||||||
for (String key : extras.keySet()) {
|
for (String key : extras.keySet()) {
|
||||||
Object value = extras.get(key);
|
@Nullable Object value = extras.get(key);
|
||||||
if (value instanceof String) {
|
if (value instanceof String) {
|
||||||
builder.putString(metadataExtrasPrefix + key, (String) value);
|
builder.putString(metadataExtrasPrefix + key, (String) value);
|
||||||
} else if (value instanceof CharSequence) {
|
} else if (value instanceof CharSequence) {
|
||||||
|
|
@ -925,38 +970,40 @@ public final class MediaSessionConnector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (description.getTitle() != null) {
|
@Nullable CharSequence title = description.getTitle();
|
||||||
String title = String.valueOf(description.getTitle());
|
if (title != null) {
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
|
String titleString = String.valueOf(title);
|
||||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title);
|
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, titleString);
|
||||||
|
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, titleString);
|
||||||
}
|
}
|
||||||
if (description.getSubtitle() != null) {
|
@Nullable CharSequence subtitle = description.getSubtitle();
|
||||||
|
if (subtitle != null) {
|
||||||
builder.putString(
|
builder.putString(
|
||||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.valueOf(subtitle));
|
||||||
String.valueOf(description.getSubtitle()));
|
|
||||||
}
|
}
|
||||||
if (description.getDescription() != null) {
|
@Nullable CharSequence displayDescription = description.getDescription();
|
||||||
|
if (displayDescription != null) {
|
||||||
builder.putString(
|
builder.putString(
|
||||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION,
|
||||||
String.valueOf(description.getDescription()));
|
String.valueOf(displayDescription));
|
||||||
}
|
}
|
||||||
if (description.getIconBitmap() != null) {
|
@Nullable Bitmap iconBitmap = description.getIconBitmap();
|
||||||
builder.putBitmap(
|
if (iconBitmap != null) {
|
||||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, description.getIconBitmap());
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, iconBitmap);
|
||||||
}
|
}
|
||||||
if (description.getIconUri() != null) {
|
@Nullable Uri iconUri = description.getIconUri();
|
||||||
|
if (iconUri != null) {
|
||||||
builder.putString(
|
builder.putString(
|
||||||
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
|
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, String.valueOf(iconUri));
|
||||||
String.valueOf(description.getIconUri()));
|
|
||||||
}
|
}
|
||||||
if (description.getMediaId() != null) {
|
@Nullable String mediaId = description.getMediaId();
|
||||||
builder.putString(
|
if (mediaId != null) {
|
||||||
MediaMetadataCompat.METADATA_KEY_MEDIA_ID, description.getMediaId());
|
builder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId);
|
||||||
}
|
}
|
||||||
if (description.getMediaUri() != null) {
|
@Nullable Uri mediaUri = description.getMediaUri();
|
||||||
|
if (mediaUri != null) {
|
||||||
builder.putString(
|
builder.putString(
|
||||||
MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
|
MediaMetadataCompat.METADATA_KEY_MEDIA_URI, String.valueOf(mediaUri));
|
||||||
String.valueOf(description.getMediaUri()));
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -975,8 +1022,8 @@ public final class MediaSessionConnector {
|
||||||
// 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) {
|
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
|
||||||
int windowCount = player.getCurrentTimeline().getWindowCount();
|
int windowCount = player.getCurrentTimeline().getWindowCount();
|
||||||
int windowIndex = player.getCurrentWindowIndex();
|
int windowIndex = player.getCurrentWindowIndex();
|
||||||
if (queueNavigator != null) {
|
if (queueNavigator != null) {
|
||||||
|
|
@ -1019,6 +1066,7 @@ public final class MediaSessionConnector {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||||
|
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
|
||||||
if (currentWindowIndex != player.getCurrentWindowIndex()) {
|
if (currentWindowIndex != player.getCurrentWindowIndex()) {
|
||||||
if (queueNavigator != null) {
|
if (queueNavigator != null) {
|
||||||
queueNavigator.onCurrentWindowIndexChanged(player);
|
queueNavigator.onCurrentWindowIndexChanged(player);
|
||||||
|
|
@ -1045,19 +1093,20 @@ public final class MediaSessionConnector {
|
||||||
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
|
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PLAY)) {
|
||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
playbackPreparer.onPrepare();
|
playbackPreparer.onPrepare(/* playWhenReady= */ true);
|
||||||
}
|
}
|
||||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||||
}
|
}
|
||||||
setPlayWhenReady(/* playWhenReady= */ true);
|
controlDispatcher.dispatchSetPlayWhenReady(
|
||||||
|
Assertions.checkNotNull(player), /* playWhenReady= */ true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
|
if (canDispatchPlaybackAction(PlaybackStateCompat.ACTION_PAUSE)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ false);
|
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1180,56 +1229,49 @@ public final class MediaSessionConnector {
|
||||||
@Override
|
@Override
|
||||||
public void onPrepare() {
|
public void onPrepare() {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ false);
|
playbackPreparer.onPrepare(/* playWhenReady= */ false);
|
||||||
playbackPreparer.onPrepare();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ false);
|
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
|
||||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareFromSearch(String query, Bundle extras) {
|
public void onPrepareFromSearch(String query, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ false);
|
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
|
||||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ false);
|
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
|
||||||
playbackPreparer.onPrepareFromUri(uri, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayFromMediaId(String mediaId, Bundle extras) {
|
public void onPlayFromMediaId(String mediaId, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ true);
|
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
|
||||||
playbackPreparer.onPrepareFromMediaId(mediaId, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayFromSearch(String query, Bundle extras) {
|
public void onPlayFromSearch(String query, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ true);
|
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
|
||||||
playbackPreparer.onPrepareFromSearch(query, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayFromUri(Uri uri, Bundle extras) {
|
public void onPlayFromUri(Uri uri, Bundle extras) {
|
||||||
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
|
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
|
||||||
setPlayWhenReady(/* playWhenReady= */ true);
|
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
|
||||||
playbackPreparer.onPrepareFromUri(uri, extras);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
@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
|
||||||
|
|
||||||
@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);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
|
||||||
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 {
|
||||||
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;
|
|
||||||
|
|
||||||
private @Nullable DataSpec dataSpec;
|
@Nullable private Predicate<String> contentTypePredicate;
|
||||||
private @Nullable Response response;
|
@Nullable private DataSpec dataSpec;
|
||||||
private @Nullable InputStream responseByteStream;
|
@Nullable private Response response;
|
||||||
|
@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 {
|
||||||
* @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 {
|
||||||
* @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 {
|
||||||
* 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 {
|
||||||
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;
|
||||||
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 {
|
||||||
new OkHttpDataSource(
|
new OkHttpDataSource(
|
||||||
callFactory,
|
callFactory,
|
||||||
userAgent,
|
userAgent,
|
||||||
/* contentTypePredicate= */ null,
|
|
||||||
cacheControl,
|
cacheControl,
|
||||||
defaultRequestProperties);
|
defaultRequestProperties);
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue