Merge branch 'dev-v2' of https://github.com/google/ExoPlayer into dev-v2
62
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Issue template for a bug report.
|
||||||
|
title: ''
|
||||||
|
labels: bug, needs triage
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
Before filing a bug:
|
||||||
|
-----------------------
|
||||||
|
- Search existing issues, including issues that are closed:
|
||||||
|
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||||
|
- Consult our developer website, which can be found at https://exoplayer.dev/.
|
||||||
|
It provides detailed information about supported formats and devices.
|
||||||
|
- Learn how to create useful log output by using the EventLogger:
|
||||||
|
https://exoplayer.dev/listening-to-player-events.html#using-eventlogger
|
||||||
|
- Rule out issues in your own code. A good way to do this is to try and
|
||||||
|
reproduce the issue in the ExoPlayer demo app. Information about the ExoPlayer
|
||||||
|
demo app can be found here:
|
||||||
|
http://exoplayer.dev/demo-application.html.
|
||||||
|
|
||||||
|
When reporting a bug:
|
||||||
|
-----------------------
|
||||||
|
Fill out the sections below, leaving the headers but replacing the content. If
|
||||||
|
you're unable to provide certain information, please explain why in the relevant
|
||||||
|
section. We may close issues if they do not include sufficient information.
|
||||||
|
|
||||||
|
### [REQUIRED] Issue description
|
||||||
|
Describe the issue in detail, including observed and expected behavior.
|
||||||
|
|
||||||
|
### [REQUIRED] Reproduction steps
|
||||||
|
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app
|
||||||
|
or a small sample app that you’re able to share as source code on GitHub.
|
||||||
|
|
||||||
|
### [REQUIRED] Link to test content
|
||||||
|
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
|
||||||
|
media that reproduces the issue. If you don't wish to post it publicly, please
|
||||||
|
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
|
||||||
|
in the format "Issue #1234", where "#1234" should be replaced with your issue
|
||||||
|
number. Provide all the metadata we'd need to play the content like drm license
|
||||||
|
urls or similar. If the content is accessible only in certain countries or
|
||||||
|
regions, please say so.
|
||||||
|
|
||||||
|
### [REQUIRED] A full bug report captured from the device
|
||||||
|
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
|
||||||
|
log snippet is NOT sufficient. Please attach the captured bug report as a file.
|
||||||
|
If you don't wish to post it publicly, please submit the issue, then email the
|
||||||
|
bug report to dev.exoplayer@gmail.com using a subject in the format
|
||||||
|
"Issue #1234", where "#1234" should be replaced with your issue number.
|
||||||
|
|
||||||
|
### [REQUIRED] Version of ExoPlayer being used
|
||||||
|
Specify the absolute version number. Avoid using terms such as "latest".
|
||||||
|
|
||||||
|
### [REQUIRED] Device(s) and version(s) of Android being used
|
||||||
|
Specify the devices and versions of Android on which the issue can be
|
||||||
|
reproduced, and how easily it reproduces. If possible, please test on multiple
|
||||||
|
devices and Android versions.
|
||||||
|
|
||||||
|
<!-- DO NOT DELETE
|
||||||
|
validate_template=true
|
||||||
|
template_path=.github/ISSUE_TEMPLATE/bug.md
|
||||||
|
-->
|
||||||
58
.github/ISSUE_TEMPLATE/content_not_playing.md
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
name: Content not playing correctly
|
||||||
|
about: Issue template for a content not playing issue.
|
||||||
|
title: ''
|
||||||
|
labels: content not playing, needs triage
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
Before filing a content issue:
|
||||||
|
------------------------------
|
||||||
|
- 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
|
||||||
|
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
|
||||||
|
ExoPlayer demo app can be found here:
|
||||||
|
http://exoplayer.dev/demo-application.html.
|
||||||
|
|
||||||
|
When reporting a content issue:
|
||||||
|
-----------------------------
|
||||||
|
Fill out the sections below, leaving the headers but replacing the content. If
|
||||||
|
you're unable to provide certain information, please explain why in the relevant
|
||||||
|
section. We may close issues if they do not include sufficient information.
|
||||||
|
|
||||||
|
### [REQUIRED] Content description
|
||||||
|
Describe the content and any specifics you expected to play but did not. This
|
||||||
|
could be the container or sample format itself or any features the stream has
|
||||||
|
and you expect to play, like 5.1 audio track, text tracks or drm systems.
|
||||||
|
|
||||||
|
### [REQUIRED] Link to test content
|
||||||
|
Provide a JSON snippet for the demo app’s media.exolist.json file, or a link to
|
||||||
|
media that reproduces the issue. If you don't wish to post it publicly, please
|
||||||
|
submit the issue, then email the link to dev.exoplayer@gmail.com using a subject
|
||||||
|
in the format "Issue #1234", where "#1234" should be replaced with your issue
|
||||||
|
number. Provide all the metadata we'd need to play the content like drm license
|
||||||
|
urls or similar. If the content is accessible only in certain countries or
|
||||||
|
regions, please say so.
|
||||||
|
|
||||||
|
### [REQUIRED] Version of ExoPlayer being used
|
||||||
|
Specify the absolute version number. Avoid using terms such as "latest".
|
||||||
|
|
||||||
|
### [REQUIRED] Device(s) and version(s) of Android being used
|
||||||
|
Specify the devices and versions of Android on which you expect the content to
|
||||||
|
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
|
||||||
|
validate_template=true
|
||||||
|
template_path=.github/ISSUE_TEMPLATE/content_not_playing.md
|
||||||
|
-->
|
||||||
35
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Issue template for a feature request.
|
||||||
|
title: ''
|
||||||
|
labels: enhancement, needs triage
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
Before filing a feature request:
|
||||||
|
-----------------------
|
||||||
|
- Search existing open issues, specifically with the label ‘enhancement’:
|
||||||
|
https://github.com/google/ExoPlayer/labels/enhancement
|
||||||
|
- Search existing pull requests: https://github.com/google/ExoPlayer/pulls
|
||||||
|
|
||||||
|
When filing a feature request:
|
||||||
|
-----------------------
|
||||||
|
Fill out the sections below, leaving the headers but replacing the content. If
|
||||||
|
you're unable to provide certain information, please explain why in the relevant
|
||||||
|
section. We may close issues if they do not include sufficient information.
|
||||||
|
|
||||||
|
### [REQUIRED] Use case description
|
||||||
|
Describe the use case or problem you are trying to solve in detail. If there are
|
||||||
|
any standards or specifications involved, please provide the relevant details.
|
||||||
|
|
||||||
|
### Proposed solution
|
||||||
|
A clear and concise description of your proposed solution, if you have one.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
A clear and concise description of any alternative solutions you considered,
|
||||||
|
if applicable.
|
||||||
|
|
||||||
|
<!-- DO NOT DELETE
|
||||||
|
validate_template=true
|
||||||
|
template_path=.github/ISSUE_TEMPLATE/feature_request.md
|
||||||
|
-->
|
||||||
55
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Issue template for a question.
|
||||||
|
title: ''
|
||||||
|
labels: question, needs triage
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
Before filing a question:
|
||||||
|
-----------------------
|
||||||
|
- This issue tracker is intended ExoPlayer specific questions. If you're asking
|
||||||
|
a general Android development question, please do so on Stack Overflow.
|
||||||
|
- Search existing issues, including issues that are closed. It’s often the
|
||||||
|
quickest way to get an answer!
|
||||||
|
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
|
||||||
|
- Consult our developer website, which can be found at https://exoplayer.dev/.
|
||||||
|
It provides detailed information about supported formats, devices as well as
|
||||||
|
information about how to use the ExoPlayer library.
|
||||||
|
- The ExoPlayer library Javadoc can be found at
|
||||||
|
https://exoplayer.dev/doc/reference/
|
||||||
|
|
||||||
|
When filing a question:
|
||||||
|
-----------------------
|
||||||
|
Fill out the sections below, leaving the headers but replacing the content. If
|
||||||
|
you're unable to provide certain information, please explain why in the relevant
|
||||||
|
section. We may close issues if they do not include sufficient information.
|
||||||
|
|
||||||
|
### [REQUIRED] Searched documentation and issues
|
||||||
|
Tell us where you’ve already looked for an answer to your question. It’s
|
||||||
|
important for us to know this so that we can improve our documentation.
|
||||||
|
|
||||||
|
### [REQUIRED] Question
|
||||||
|
Describe your question in detail.
|
||||||
|
|
||||||
|
### A full bug report captured from the device
|
||||||
|
In case your question refers to a problem you are seeing in your app, capture a
|
||||||
|
full bug report using "adb bugreport". Please attach the captured bug report as
|
||||||
|
a file. If you don't wish to post it publicly, please submit the issue, then
|
||||||
|
email the bug report to dev.exoplayer@gmail.com using a subject in the format
|
||||||
|
"Issue #1234", where "#1234" should be replaced with your issue number.
|
||||||
|
|
||||||
|
### Link to test content
|
||||||
|
In case your question is related to a piece of media, which you are trying to
|
||||||
|
play, please provide a JSON snippet for the demo app’s media.exolist.json file,
|
||||||
|
or a link to media that reproduces the issue. If you don't wish to post it
|
||||||
|
publicly, please submit the issue, then email the link to
|
||||||
|
dev.exoplayer@gmail.com using a subject in the format "Issue #1234", where
|
||||||
|
"#1234" should be replaced with your issue number. Provide all the metadata we'd
|
||||||
|
need to play the content like drm license urls or similar. If the content is
|
||||||
|
accessible only in certain countries or regions, please say so.
|
||||||
|
|
||||||
|
<!-- DO NOT DELETE
|
||||||
|
validate_template=true
|
||||||
|
template_path=.github/ISSUE_TEMPLATE/question.md
|
||||||
|
-->
|
||||||
9
.gitignore
vendored
|
|
@ -37,6 +37,12 @@ local.properties
|
||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
|
|
@ -66,3 +72,6 @@ extensions/cronet/jniLibs/*
|
||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
|
|
||||||
10
.hgignore
|
|
@ -44,6 +44,12 @@ local.properties
|
||||||
proguard.cfg
|
proguard.cfg
|
||||||
proguard-project.txt
|
proguard-project.txt
|
||||||
|
|
||||||
|
# Bazel
|
||||||
|
bazel-bin
|
||||||
|
bazel-genfiles
|
||||||
|
bazel-out
|
||||||
|
bazel-testlogs
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cmake-build-debug
|
cmake-build-debug
|
||||||
|
|
@ -69,3 +75,7 @@ extensions/cronet/jniLibs/*
|
||||||
!extensions/cronet/jniLibs/README.md
|
!extensions/cronet/jniLibs/README.md
|
||||||
extensions/cronet/libs/*
|
extensions/cronet/libs/*
|
||||||
!extensions/cronet/libs/README.md
|
!extensions/cronet/libs/README.md
|
||||||
|
|
||||||
|
# Cast receiver
|
||||||
|
cast_receiver_app/external-js
|
||||||
|
cast_receiver_app/bazel-cast_receiver_app
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,8 @@ all of the information requested in the issue template.
|
||||||
## Pull requests ##
|
## Pull requests ##
|
||||||
|
|
||||||
We will also consider high quality pull requests. These should normally merge
|
We will also consider high quality pull requests. These should normally merge
|
||||||
into the `dev-vX` branch with the highest major version number. Bug fixes may
|
into the `dev-v2` branch. Before a pull request can be accepted you must submit
|
||||||
be suitable for merging into older `dev-vX` branches. Before a pull request can
|
a Contributor License Agreement, as described below.
|
||||||
be accepted you must submit a Contributor License Agreement, as described below.
|
|
||||||
|
|
||||||
[dev]: https://github.com/google/ExoPlayer/tree/dev
|
[dev]: https://github.com/google/ExoPlayer/tree/dev
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
Before filing an issue:
|
|
||||||
-----------------------
|
|
||||||
- Search existing issues, including issues that are closed.
|
|
||||||
- Consult our FAQs, supported devices and supported formats pages. These can be
|
|
||||||
found at https://google.github.io/ExoPlayer/.
|
|
||||||
- Rule out issues in your own code. A good way to do this is to try and
|
|
||||||
reproduce the issue in the ExoPlayer demo app.
|
|
||||||
- This issue tracker is intended for bugs, feature requests and ExoPlayer
|
|
||||||
specific questions. If you're asking a general Android development question,
|
|
||||||
please do so on Stack Overflow.
|
|
||||||
|
|
||||||
When reporting a bug:
|
|
||||||
-----------------------
|
|
||||||
Fill out the sections below, leaving the headers but replacing the content. If
|
|
||||||
you're unable to provide certain information, please explain why in the relevant
|
|
||||||
section. We may close issues if they do not include sufficient information.
|
|
||||||
|
|
||||||
### Issue description
|
|
||||||
Describe the issue in detail, including observed and expected behavior.
|
|
||||||
|
|
||||||
### Reproduction steps
|
|
||||||
Describe how the issue can be reproduced, ideally using the ExoPlayer demo app.
|
|
||||||
|
|
||||||
### Link to test content
|
|
||||||
Provide a link to media that reproduces the issue. If you don't wish to post it
|
|
||||||
publicly, please submit the issue, then email the link to
|
|
||||||
dev.exoplayer@gmail.com using a subject in the format "Issue #1234".
|
|
||||||
|
|
||||||
### Version of ExoPlayer being used
|
|
||||||
Specify the absolute version number. Avoid using terms such as "latest".
|
|
||||||
|
|
||||||
### Device(s) and version(s) of Android being used
|
|
||||||
Specify the devices and versions of Android on which the issue can be
|
|
||||||
reproduced, and how easily it reproduces. If possible, please test on multiple
|
|
||||||
devices and Android versions.
|
|
||||||
|
|
||||||
### A full bug report captured from the device
|
|
||||||
Capture a full bug report using "adb bugreport". Output from "adb logcat" or a
|
|
||||||
log snippet is NOT sufficient. Please attach the captured bug report as a file.
|
|
||||||
If you don't wish to post it publicly, please submit the issue, then email the
|
|
||||||
bug report to dev.exoplayer@gmail.com using a subject in the format
|
|
||||||
"Issue #1234".
|
|
||||||
|
|
||||||
34
README.md
|
|
@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates.
|
||||||
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
|
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer
|
||||||
developments!
|
developments!
|
||||||
|
|
||||||
[developer guide]: https://google.github.io/ExoPlayer/guide.html
|
[developer guide]: https://exoplayer.dev/guide.html
|
||||||
[class reference]: https://google.github.io/ExoPlayer/doc/reference
|
[class reference]: https://exoplayer.dev/doc/reference
|
||||||
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
|
||||||
[developer blog]: https://medium.com/google-exoplayer
|
[developer blog]: https://medium.com/google-exoplayer
|
||||||
|
|
||||||
|
|
@ -27,17 +27,21 @@ repository and depend on the modules locally.
|
||||||
|
|
||||||
### From JCenter ###
|
### From JCenter ###
|
||||||
|
|
||||||
|
#### 1. Add repositories ####
|
||||||
|
|
||||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||||
dependency. You need to make sure you have the JCenter and Google repositories
|
dependency. You need to make sure you have the Google and JCenter repositories
|
||||||
included in the `build.gradle` file in the root of your project:
|
included in the `build.gradle` file in the root of your project:
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
|
||||||
google()
|
google()
|
||||||
|
jcenter()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 2. Add ExoPlayer module dependencies ####
|
||||||
|
|
||||||
Next add a dependency in the `build.gradle` file of your app module. The
|
Next add a dependency in the `build.gradle` file of your app module. The
|
||||||
following will add a dependency to the full library:
|
following will add a dependency to the full library:
|
||||||
|
|
||||||
|
|
@ -45,10 +49,12 @@ following will add a dependency to the full library:
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||||
```
|
```
|
||||||
|
|
||||||
where `2.X.X` is your preferred version. Alternatively, you can depend on only
|
where `2.X.X` is your preferred version.
|
||||||
the library modules that you actually need. For example the following will add
|
|
||||||
dependencies on the Core, DASH and UI library modules, as might be required for
|
As an alternative to the full library, you can depend on only the library
|
||||||
an app that plays DASH content:
|
modules that you actually need. For example the following will add dependencies
|
||||||
|
on the Core, DASH and UI library modules, as might be required for an app that
|
||||||
|
plays DASH content:
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
|
implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
|
||||||
|
|
@ -77,6 +83,18 @@ JCenter can be found on [Bintray][].
|
||||||
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||||
[Bintray]: https://bintray.com/google/exoplayer
|
[Bintray]: https://bintray.com/google/exoplayer
|
||||||
|
|
||||||
|
#### 3. Turn on Java 8 support ####
|
||||||
|
|
||||||
|
If not enabled already, you also need to turn on Java 8 support in all
|
||||||
|
`build.gradle` files depending on ExoPlayer, by adding the following to the
|
||||||
|
`android` section:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Locally ###
|
### Locally ###
|
||||||
|
|
||||||
Cloning the repository and depending on the modules locally is required when
|
Cloning the repository and depending on the modules locally is required when
|
||||||
|
|
|
||||||
687
RELEASENOTES.md
|
|
@ -2,144 +2,610 @@
|
||||||
|
|
||||||
### dev-v2 (not yet released) ###
|
### dev-v2 (not yet released) ###
|
||||||
|
|
||||||
* Add a flag to opt-in to automatic audio focus handling via
|
* 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
|
||||||
|
checks ([#5568](https://github.com/google/ExoPlayer/issues/5568)).
|
||||||
|
* Decoders: Prefer decoders that advertise format support over ones that do not,
|
||||||
|
even if they are listed lower in the `MediaCodecList`.
|
||||||
|
* Add a workaround for broken raw audio decoding on Oppo R9
|
||||||
|
([#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 ###
|
||||||
|
|
||||||
|
* Offline: Add option to remove all downloads.
|
||||||
|
* HLS: Fix `NullPointerException` when using HLS chunkless preparation
|
||||||
|
([#5868](https://github.com/google/ExoPlayer/issues/5868)).
|
||||||
|
* Fix handling of empty values and line terminators in SHOUTcast ICY metadata
|
||||||
|
([#5876](https://github.com/google/ExoPlayer/issues/5876)).
|
||||||
|
* Fix DVB subtitles for SDK 28
|
||||||
|
([#5862](https://github.com/google/ExoPlayer/issues/5862)).
|
||||||
|
* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing
|
||||||
|
48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)).
|
||||||
|
|
||||||
|
### 2.10.0 ###
|
||||||
|
|
||||||
|
* Core library:
|
||||||
|
* Improve decoder re-use between playbacks
|
||||||
|
([#2826](https://github.com/google/ExoPlayer/issues/2826)). Read
|
||||||
|
[this blog post](https://medium.com/google-exoplayer/improved-decoder-reuse-in-exoplayer-ef4c6d99591d)
|
||||||
|
for more details.
|
||||||
|
* Rename `ExtractorMediaSource` to `ProgressiveMediaSource`.
|
||||||
|
* Fix issue where using `ProgressiveMediaSource.Factory` would mean that
|
||||||
|
`DefaultExtractorsFactory` would be kept by proguard. Custom
|
||||||
|
`ExtractorsFactory` instances must now be passed via the
|
||||||
|
`ProgressiveMediaSource.Factory` constructor, and `setExtractorsFactory` is
|
||||||
|
deprecated.
|
||||||
|
* Move `PriorityTaskManager` from `DefaultLoadControl` to `SimpleExoPlayer`.
|
||||||
|
* Add new `ExoPlaybackException` types for remote exceptions and out-of-memory
|
||||||
|
errors.
|
||||||
|
* Use full BCP 47 language tags in `Format`.
|
||||||
|
* Do not retry failed loads whose error is `FileNotFoundException`.
|
||||||
|
* Fix issue where not resetting the position for a new `MediaSource` in calls
|
||||||
|
to `ExoPlayer.prepare` causes an `IndexOutOfBoundsException`
|
||||||
|
([#5520](https://github.com/google/ExoPlayer/issues/5520)).
|
||||||
|
* Offline:
|
||||||
|
* Improve offline support. `DownloadManager` now tracks all offline content,
|
||||||
|
not just tasks in progress. Read
|
||||||
|
[this page](https://exoplayer.dev/downloading-media.html) for more details.
|
||||||
|
* Caching:
|
||||||
|
* Improve performance of `SimpleCache`
|
||||||
|
([#4253](https://github.com/google/ExoPlayer/issues/4253)).
|
||||||
|
* Cache data with unknown length by default. The previous flag to opt in to
|
||||||
|
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
|
||||||
|
replaced with an opt out flag
|
||||||
|
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
|
||||||
|
* Extractors:
|
||||||
|
* MP4/FMP4: Add support for Dolby Vision.
|
||||||
|
* MP4: Fix issue handling meta atoms in some streams
|
||||||
|
([#5698](https://github.com/google/ExoPlayer/issues/5698),
|
||||||
|
[#5694](https://github.com/google/ExoPlayer/issues/5694)).
|
||||||
|
* MP3: Add support for SHOUTcast ICY metadata
|
||||||
|
([#3735](https://github.com/google/ExoPlayer/issues/3735)).
|
||||||
|
* MP3: Fix ID3 frame unsychronization
|
||||||
|
([#5673](https://github.com/google/ExoPlayer/issues/5673)).
|
||||||
|
* MP3: Fix playback of badly clipped files
|
||||||
|
([#5772](https://github.com/google/ExoPlayer/issues/5772)).
|
||||||
|
* MPEG-TS: Enable HDMV DTS stream detection only if a flag is set. By default
|
||||||
|
(i.e. if the flag is not set), the 0x82 elementary stream type is now
|
||||||
|
treated as an SCTE subtitle track
|
||||||
|
([#5330](https://github.com/google/ExoPlayer/issues/5330)).
|
||||||
|
* Track selection:
|
||||||
|
* Add options for controlling audio track selections to `DefaultTrackSelector`
|
||||||
|
([#3314](https://github.com/google/ExoPlayer/issues/3314)).
|
||||||
|
* Update `TrackSelection.Factory` interface to support creating all track
|
||||||
|
selections together.
|
||||||
|
* Allow to specify a selection reason for a `SelectionOverride`.
|
||||||
|
* When no text language preference matches, only select forced text tracks
|
||||||
|
whose language matches the selected audio language.
|
||||||
|
* UI:
|
||||||
|
* Update `DefaultTimeBar` based on duration of media and add parameter to set
|
||||||
|
the minimum update interval to control the smoothness of the updates
|
||||||
|
([#5040](https://github.com/google/ExoPlayer/issues/5040)).
|
||||||
|
* Move creation of dialogs for `TrackSelectionView`s to
|
||||||
|
`TrackSelectionDialogBuilder` and add option to select multiple overrides.
|
||||||
|
* Change signature of `PlayerNotificationManager.NotificationListener` to
|
||||||
|
better fit service requirements.
|
||||||
|
* Add option to include navigation actions in the compact mode of
|
||||||
|
notifications created using `PlayerNotificationManager`.
|
||||||
|
* Fix issues with flickering notifications on KitKat when using
|
||||||
|
`PlayerNotificationManager` and `DownloadNotificationUtil`. For the latter,
|
||||||
|
applications should switch to using `DownloadNotificationHelper`.
|
||||||
|
* Fix accuracy of D-pad seeking in `DefaultTimeBar`
|
||||||
|
([#5767](https://github.com/google/ExoPlayer/issues/5767)).
|
||||||
|
* Audio:
|
||||||
|
* Allow `AudioProcessor`s to be drained of pending output after they are
|
||||||
|
reconfigured.
|
||||||
|
* Fix an issue that caused audio to be truncated at the end of a period
|
||||||
|
when switching to a new period where gapless playback information was newly
|
||||||
|
present or newly absent.
|
||||||
|
* Add support for reading AC-4 streams
|
||||||
|
([#5303](https://github.com/google/ExoPlayer/pull/5303)).
|
||||||
|
* Video:
|
||||||
|
* Remove `MediaCodecSelector.DEFAULT_WITH_FALLBACK`. Apps should instead
|
||||||
|
signal that fallback should be used by passing `true` as the
|
||||||
|
`enableDecoderFallback` parameter when instantiating the video renderer.
|
||||||
|
* Support video tunneling when the decoder is not listed first for the MIME
|
||||||
|
type ([#3100](https://github.com/google/ExoPlayer/issues/3100)).
|
||||||
|
* Query `MediaCodecList.ALL_CODECS` when selecting a tunneling decoder
|
||||||
|
([#5547](https://github.com/google/ExoPlayer/issues/5547)).
|
||||||
|
* DRM:
|
||||||
|
* Fix black flicker when keys rotate in DRM protected content
|
||||||
|
([#3561](https://github.com/google/ExoPlayer/issues/3561)).
|
||||||
|
* Work around lack of LA_URL attribute in PlayReady key request init data.
|
||||||
|
* CEA-608: Improved conformance to the specification
|
||||||
|
([#3860](https://github.com/google/ExoPlayer/issues/3860)).
|
||||||
|
* DASH:
|
||||||
|
* Parse role and accessibility descriptors into `Format.roleFlags`.
|
||||||
|
* Support multiple CEA-608 channels muxed into FMP4 representations
|
||||||
|
([#5656](https://github.com/google/ExoPlayer/issues/5656)).
|
||||||
|
* HLS:
|
||||||
|
* Prevent unnecessary reloads of initialization segments.
|
||||||
|
* Form an adaptive track group out of audio renditions with matching name.
|
||||||
|
* Support encrypted initialization segments
|
||||||
|
([#5441](https://github.com/google/ExoPlayer/issues/5441)).
|
||||||
|
* Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`.
|
||||||
|
* Add metadata entry for HLS tracks to expose master playlist information.
|
||||||
|
* Prevent `IndexOutOfBoundsException` in some live HLS scenarios
|
||||||
|
([#5816](https://github.com/google/ExoPlayer/issues/5816)).
|
||||||
|
* Support for playing spherical videos on Daydream.
|
||||||
|
* Cast extension: Work around Cast framework returning a limited-size queue
|
||||||
|
items list ([#4964](https://github.com/google/ExoPlayer/issues/4964)).
|
||||||
|
* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to
|
||||||
|
surface YUV output as the default. Remove constructor parameters `scaleToFit`
|
||||||
|
and `useSurfaceYuvOutput`.
|
||||||
|
* MediaSession extension:
|
||||||
|
* Let apps intercept media button events
|
||||||
|
([#5179](https://github.com/google/ExoPlayer/issues/5179)).
|
||||||
|
* Fix issue with `TimelineQueueNavigator` not publishing the queue in shuffled
|
||||||
|
order when in shuffle mode.
|
||||||
|
* Allow handling of custom commands via `registerCustomCommandReceiver`.
|
||||||
|
* Add ability to include an extras `Bundle` when reporting a custom error.
|
||||||
|
* LoadControl: Set minimum buffer for playbacks with video equal to maximum
|
||||||
|
buffer ([#2083](https://github.com/google/ExoPlayer/issues/2083)).
|
||||||
|
* Log warnings when extension native libraries can't be used, to help with
|
||||||
|
diagnosing playback failures
|
||||||
|
([#5788](https://github.com/google/ExoPlayer/issues/5788)).
|
||||||
|
|
||||||
|
### 2.9.6 ###
|
||||||
|
|
||||||
|
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`.
|
||||||
|
* IMA extension:
|
||||||
|
* Require setting the `Player` on `AdsLoader` instances before
|
||||||
|
playback.
|
||||||
|
* Remove deprecated `ImaAdsMediaSource`. Create `AdsMediaSource` with an
|
||||||
|
`ImaAdsLoader` instead.
|
||||||
|
* Remove deprecated `AdsMediaSource` constructors. Listen for media source
|
||||||
|
events using `AdsMediaSource.addEventListener`, and ad interaction events by
|
||||||
|
adding a listener when building `ImaAdsLoader`.
|
||||||
|
* Allow apps to register playback-related obstructing views that are on top of
|
||||||
|
their ad display containers via `AdsLoader.AdViewProvider`. `PlayerView`
|
||||||
|
implements this interface and will register its control view. This makes it
|
||||||
|
possible for ad loading SDKs to calculate ad viewability accurately.
|
||||||
|
* DASH: Fix issue handling large `EventStream` presentation timestamps
|
||||||
|
([#5490](https://github.com/google/ExoPlayer/issues/5490)).
|
||||||
|
* HLS: Fix transition to STATE_ENDED when playing fragmented mp4 in chunkless
|
||||||
|
preparation ([#5524](https://github.com/google/ExoPlayer/issues/5524)).
|
||||||
|
* Revert workaround for video quality problems with Amlogic decoders, as this
|
||||||
|
may cause problems for some devices and/or non-interlaced content
|
||||||
|
([#5003](https://github.com/google/ExoPlayer/issues/5003)).
|
||||||
|
|
||||||
|
### 2.9.5 ###
|
||||||
|
|
||||||
|
* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag.
|
||||||
|
* ConcatenatingMediaSource:
|
||||||
|
* Add `Handler` parameter to methods that take a callback `Runnable`.
|
||||||
|
* Fix issue with dropped messages when releasing the source
|
||||||
|
([#5464](https://github.com/google/ExoPlayer/issues/5464)).
|
||||||
|
* ExtractorMediaSource: Fix issue that could cause the player to get stuck
|
||||||
|
buffering at the end of the media.
|
||||||
|
* PlayerView: Fix issue preventing `OnClickListener` from receiving events
|
||||||
|
([#5433](https://github.com/google/ExoPlayer/issues/5433)).
|
||||||
|
* IMA extension: Upgrade IMA dependency to 3.10.6.
|
||||||
|
* Cronet extension: Upgrade Cronet dependency to 71.3578.98.
|
||||||
|
* OkHttp extension: Upgrade OkHttp dependency to 3.12.1.
|
||||||
|
* MP3: Wider fix for issue where streams would play twice on some Samsung
|
||||||
|
devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
|
||||||
|
|
||||||
|
### 2.9.4 ###
|
||||||
|
|
||||||
|
* IMA extension: Clear ads loader listeners on release
|
||||||
|
([#4114](https://github.com/google/ExoPlayer/issues/4114)).
|
||||||
|
* SmoothStreaming: Fix support for subtitles in DRM protected streams
|
||||||
|
([#5378](https://github.com/google/ExoPlayer/issues/5378)).
|
||||||
|
* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
|
||||||
|
of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
|
||||||
|
* GVR extension: upgrade GVR SDK dependency to 1.190.0.
|
||||||
|
* Associate fatal player errors of type SOURCE with the loading source in
|
||||||
|
`AnalyticsListener.EventTime`
|
||||||
|
([#5407](https://github.com/google/ExoPlayer/issues/5407)).
|
||||||
|
* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where
|
||||||
|
using lazy preparation in `ConcatenatingMediaSource` with an
|
||||||
|
`ExtractorMediaSource` overrides initial seek positions
|
||||||
|
([#5350](https://github.com/google/ExoPlayer/issues/5350)).
|
||||||
|
* Add subtext to the `MediaDescriptionAdapter` of the
|
||||||
|
`PlayerNotificationManager`.
|
||||||
|
* Add workaround for video quality problems with Amlogic decoders
|
||||||
|
([#5003](https://github.com/google/ExoPlayer/issues/5003)).
|
||||||
|
* Fix issue where sending callbacks for playlist changes may cause problems
|
||||||
|
because of parallel player access
|
||||||
|
([#5240](https://github.com/google/ExoPlayer/issues/5240)).
|
||||||
|
* Fix issue with reusing a `ClippingMediaSource` with an inner
|
||||||
|
`ExtractorMediaSource` and a non-zero start position
|
||||||
|
([#5351](https://github.com/google/ExoPlayer/issues/5351)).
|
||||||
|
* Fix issue where uneven track durations in MP4 streams can cause OOM problems
|
||||||
|
([#3670](https://github.com/google/ExoPlayer/issues/3670)).
|
||||||
|
|
||||||
|
### 2.9.3 ###
|
||||||
|
|
||||||
|
* Captions: Support PNG subtitles in SMPTE-TT
|
||||||
|
([#1583](https://github.com/google/ExoPlayer/issues/1583)).
|
||||||
|
* MPEG-TS: Use random access indicators to minimize the need for
|
||||||
|
`FLAG_ALLOW_NON_IDR_KEYFRAMES`.
|
||||||
|
* Downloading: Reduce time taken to remove downloads
|
||||||
|
([#5136](https://github.com/google/ExoPlayer/issues/5136)).
|
||||||
|
* MP3:
|
||||||
|
* Use the true bitrate for constant-bitrate MP3 seeking.
|
||||||
|
* Fix issue where streams would play twice on some Samsung devices
|
||||||
|
([#4519](https://github.com/google/ExoPlayer/issues/4519)).
|
||||||
|
* Fix regression where some audio formats were incorrectly marked as being
|
||||||
|
unplayable due to under-reporting of platform decoder capabilities
|
||||||
|
([#5145](https://github.com/google/ExoPlayer/issues/5145)).
|
||||||
|
* Fix decode-only frame skipping on Nvidia Shield TV devices.
|
||||||
|
* Workaround for MiTV (dangal) issue when swapping output surface
|
||||||
|
([#5169](https://github.com/google/ExoPlayer/issues/5169)).
|
||||||
|
|
||||||
|
### 2.9.2 ###
|
||||||
|
|
||||||
|
* HLS:
|
||||||
|
* Fix issue causing unnecessary media playlist requests when playing live
|
||||||
|
streams ([#5059](https://github.com/google/ExoPlayer/issues/5059)).
|
||||||
|
* Fix decoder re-instantiation issue for packed audio streams
|
||||||
|
([#5063](https://github.com/google/ExoPlayer/issues/5063)).
|
||||||
|
* MP4: Support Opus and FLAC in the MP4 container, and in DASH
|
||||||
|
([#4883](https://github.com/google/ExoPlayer/issues/4883)).
|
||||||
|
* DASH: Fix detecting the end of live events
|
||||||
|
([#4780](https://github.com/google/ExoPlayer/issues/4780)).
|
||||||
|
* Spherical video: Fall back to `TYPE_ROTATION_VECTOR` if
|
||||||
|
`TYPE_GAME_ROTATION_VECTOR` is unavailable
|
||||||
|
([#5119](https://github.com/google/ExoPlayer/issues/5119)).
|
||||||
|
* Support seeking for a wider range of MPEG-TS streams
|
||||||
|
([#5097](https://github.com/google/ExoPlayer/issues/5097)).
|
||||||
|
* Include channel count in audio capabilities check
|
||||||
|
([#4690](https://github.com/google/ExoPlayer/issues/4690)).
|
||||||
|
* Fix issue with applying the `show_buffering` attribute in `PlayerView`
|
||||||
|
([#5139](https://github.com/google/ExoPlayer/issues/5139)).
|
||||||
|
* Fix issue where null `Metadata` was output when it failed to decode
|
||||||
|
([#5149](https://github.com/google/ExoPlayer/issues/5149)).
|
||||||
|
* Fix playback of some invalid but playable MP4 streams by replacing assertions
|
||||||
|
with logged warnings in sample table parsing code
|
||||||
|
([#5162](https://github.com/google/ExoPlayer/issues/5162)).
|
||||||
|
* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27.
|
||||||
|
|
||||||
|
### 2.9.1 ###
|
||||||
|
|
||||||
|
* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext`
|
||||||
|
and `Player.hasPrevious`
|
||||||
|
([#4863](https://github.com/google/ExoPlayer/issues/4863)).
|
||||||
|
* Improve initial bandwidth meter estimates using the current country and
|
||||||
|
network type.
|
||||||
|
* IMA extension:
|
||||||
|
* For preroll to live stream transitions, project forward the loading position
|
||||||
|
to avoid being behind the live window.
|
||||||
|
* Let apps specify whether to focus the skip button on ATV
|
||||||
|
([#5019](https://github.com/google/ExoPlayer/issues/5019)).
|
||||||
|
* MP3:
|
||||||
|
* Support seeking based on MLLT metadata
|
||||||
|
([#3241](https://github.com/google/ExoPlayer/issues/3241)).
|
||||||
|
* Fix handling of streams with appended data
|
||||||
|
([#4954](https://github.com/google/ExoPlayer/issues/4954)).
|
||||||
|
* DASH: Parse ProgramInformation element if present in the manifest.
|
||||||
|
* HLS:
|
||||||
|
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
|
||||||
|
reader factory flags
|
||||||
|
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
|
||||||
|
* Fix bug in segment sniffing
|
||||||
|
([#5039](https://github.com/google/ExoPlayer/issues/5039)).
|
||||||
|
* SubRip: Add support for alignment tags, and remove tags from the displayed
|
||||||
|
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
|
||||||
|
* Fix issue with blind seeking to windows with non-zero offset in a
|
||||||
|
`ConcatenatingMediaSource`
|
||||||
|
([#4873](https://github.com/google/ExoPlayer/issues/4873)).
|
||||||
|
* Fix logic for enabling next and previous actions in `TimelineQueueNavigator`
|
||||||
|
([#5065](https://github.com/google/ExoPlayer/issues/5065)).
|
||||||
|
* Fix issue where audio focus handling could not be disabled after enabling it
|
||||||
|
([#5055](https://github.com/google/ExoPlayer/issues/5055)).
|
||||||
|
* Fix issue where subtitles were positioned incorrectly if `SubtitleView` had a
|
||||||
|
non-zero position offset to its parent
|
||||||
|
([#4788](https://github.com/google/ExoPlayer/issues/4788)).
|
||||||
|
* Fix issue where the buffered position was not updated correctly when
|
||||||
|
transitioning between periods
|
||||||
|
([#4899](https://github.com/google/ExoPlayer/issues/4899)).
|
||||||
|
* Fix issue where a `NullPointerException` is thrown when removing an unprepared
|
||||||
|
media source from a `ConcatenatingMediaSource` with the `useLazyPreparation`
|
||||||
|
option enabled ([#4986](https://github.com/google/ExoPlayer/issues/4986)).
|
||||||
|
* Work around an issue where a non-empty end-of-stream audio buffer would be
|
||||||
|
output with timestamp zero, causing the player position to jump backwards
|
||||||
|
([#5045](https://github.com/google/ExoPlayer/issues/5045)).
|
||||||
|
* Suppress a spurious assertion failure on some Samsung devices
|
||||||
|
([#4532](https://github.com/google/ExoPlayer/issues/4532)).
|
||||||
|
* Suppress spurious "references unknown class member" shrinking warning
|
||||||
|
([#4890](https://github.com/google/ExoPlayer/issues/4890)).
|
||||||
|
* Swap recommended order for google() and jcenter() in gradle config
|
||||||
|
([#4997](https://github.com/google/ExoPlayer/issues/4997)).
|
||||||
|
|
||||||
|
### 2.9.0 ###
|
||||||
|
|
||||||
|
* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to
|
||||||
|
add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their
|
||||||
|
gradle settings to ensure bytecode compatibility.
|
||||||
|
* Set `compileSdkVersion` and `targetSdkVersion` to 28.
|
||||||
|
* Support for automatic audio focus handling via
|
||||||
`SimpleExoPlayer.setAudioAttributes`.
|
`SimpleExoPlayer.setAudioAttributes`.
|
||||||
* Distribute Cronet extension via jCenter.
|
* Add `ExoPlayer.retry` convenience method.
|
||||||
* Set compileSdkVersion and targetSdkVersion to 28.
|
|
||||||
* Add `AudioListener` for listening to changes in audio configuration during
|
* Add `AudioListener` for listening to changes in audio configuration during
|
||||||
playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)).
|
playback ([#3994](https://github.com/google/ExoPlayer/issues/3994)).
|
||||||
* Improved seeking support:
|
* Add `LoadErrorHandlingPolicy` to allow configuration of load error handling
|
||||||
|
across `MediaSource` implementations
|
||||||
|
([#3370](https://github.com/google/ExoPlayer/issues/3370)).
|
||||||
|
* Allow passing a `Looper`, which specifies the thread that must be used to
|
||||||
|
access the player, when instantiating player instances using
|
||||||
|
`ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)).
|
||||||
|
* Allow setting log level for ExoPlayer logcat output
|
||||||
|
([#4665](https://github.com/google/ExoPlayer/issues/4665)).
|
||||||
|
* Simplify `BandwidthMeter` injection: The `BandwidthMeter` should now be
|
||||||
|
passed directly to `ExoPlayerFactory`, instead of to `TrackSelection.Factory`
|
||||||
|
and `DataSource.Factory`. The `BandwidthMeter` is passed to the components
|
||||||
|
that need it internally. The `BandwidthMeter` may also be omitted, in which
|
||||||
|
case a default instance will be used.
|
||||||
|
* Spherical video:
|
||||||
|
* Support for spherical video by setting `surface_type="spherical_view"` on
|
||||||
|
`PlayerView`.
|
||||||
|
* Support for
|
||||||
|
[VR180](https://github.com/google/spatial-media/blob/master/docs/vr180.md).
|
||||||
|
* HLS:
|
||||||
|
* Support PlayReady.
|
||||||
|
* Add container format sniffing
|
||||||
|
([#2025](https://github.com/google/ExoPlayer/issues/2025)).
|
||||||
|
* Support alternative `EXT-X-KEY` tags.
|
||||||
|
* Support `EXT-X-INDEPENDENT-SEGMENTS` in the master playlist.
|
||||||
|
* Support variable substitution
|
||||||
|
([#4422](https://github.com/google/ExoPlayer/issues/4422)).
|
||||||
|
* Fix the bitrate being unset on primary track sample formats
|
||||||
|
([#3297](https://github.com/google/ExoPlayer/issues/3297)).
|
||||||
|
* Make `HlsMediaSource.Factory` take a factory of trackers instead of a
|
||||||
|
tracker instance ([#4814](https://github.com/google/ExoPlayer/issues/4814)).
|
||||||
|
* DASH:
|
||||||
|
* Support `messageData` attribute for in-manifest event streams.
|
||||||
|
* Clip periods to their specified durations
|
||||||
|
([#4185](https://github.com/google/ExoPlayer/issues/4185)).
|
||||||
|
* Improve seeking support for progressive streams:
|
||||||
* Support seeking in MPEG-TS
|
* Support seeking in MPEG-TS
|
||||||
([#966](https://github.com/google/ExoPlayer/issues/966)).
|
([#966](https://github.com/google/ExoPlayer/issues/966)).
|
||||||
* Support seeking in MPEG-PS
|
* Support seeking in MPEG-PS
|
||||||
([#4476](https://github.com/google/ExoPlayer/issues/4476)).
|
([#4476](https://github.com/google/ExoPlayer/issues/4476)).
|
||||||
* Support approximate seeking in ADTS using a constant bitrate assumption
|
* Support approximate seeking in ADTS using a constant bitrate assumption
|
||||||
([#4548](https://github.com/google/ExoPlayer/issues/4548)). Note that the
|
([#4548](https://github.com/google/ExoPlayer/issues/4548)). The
|
||||||
`FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor to
|
`FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor to
|
||||||
enable this functionality.
|
enable this functionality.
|
||||||
* Support approximate seeking in AMR using a constant bitrate assumption.
|
* Support approximate seeking in AMR using a constant bitrate assumption.
|
||||||
Note that the `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the
|
The `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` flag must be set on the extractor
|
||||||
extractor to enable this functionality.
|
to enable this functionality.
|
||||||
* Add `DefaultExtractorsFactory.setConstantBitrateSeekingEnabled` to enable
|
* Add `DefaultExtractorsFactory.setConstantBitrateSeekingEnabled` to enable
|
||||||
approximate seeking using a constant bitrate assumption for all extractors
|
approximate seeking using a constant bitrate assumption on all extractors
|
||||||
that support it.
|
that support it.
|
||||||
* MPEG-TS: Support CEA-608/708 in H262
|
* Video:
|
||||||
([#2565](https://github.com/google/ExoPlayer/issues/2565)).
|
* Add callback to `VideoListener` to notify of surface size changes.
|
||||||
* MediaSession extension: Allow apps to set custom errors.
|
* Improve performance when playing high frame-rate content, and when playing
|
||||||
|
at greater than 1x speed
|
||||||
|
([#2777](https://github.com/google/ExoPlayer/issues/2777)).
|
||||||
|
* Scale up the initial video decoder maximum input size so playlist
|
||||||
|
transitions with small increases in maximum sample size do not require
|
||||||
|
reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)).
|
||||||
|
* Fix a bug where the player would not transition to the ended state when
|
||||||
|
playing video in tunneled mode.
|
||||||
* Audio:
|
* Audio:
|
||||||
* Add support for mu-law and A-law PCM with the ffmpeg extension
|
* Support attaching auxiliary audio effects to the `AudioTrack` via
|
||||||
|
`Player.setAuxEffectInfo` and `Player.clearAuxEffectInfo`.
|
||||||
|
* Support seamless adaptation while playing xHE-AAC streams.
|
||||||
([#4360](https://github.com/google/ExoPlayer/issues/4360)).
|
([#4360](https://github.com/google/ExoPlayer/issues/4360)).
|
||||||
* Increase `AudioTrack` buffer sizes to the theoretical maximum required for
|
* Increase `AudioTrack` buffer sizes to the theoretical maximum required for
|
||||||
each encoding for passthrough playbacks
|
each encoding for passthrough playbacks
|
||||||
([#3803](https://github.com/google/ExoPlayer/issues/3803)).
|
([#3803](https://github.com/google/ExoPlayer/issues/3803)).
|
||||||
* Add support for attaching auxiliary audio effects to the `AudioTrack`.
|
* WAV: Fix issue where white noise would be output at the end of playback
|
||||||
* Add support for seamless adaptation while playing xHE-AAC streams.
|
([#4724](https://github.com/google/ExoPlayer/issues/4724)).
|
||||||
* Video:
|
* MP3: Fix issue where streams would play twice on the SM-T530
|
||||||
* Add callback to `VideoListener` to notify of surface size changes.
|
([#4519](https://github.com/google/ExoPlayer/issues/4519)).
|
||||||
* Scale up the initial video decoder maximum input size so playlist item
|
* Analytics:
|
||||||
transitions with small increases in maximum sample size don't require
|
* Add callbacks to `DefaultDrmSessionEventListener` and `AnalyticsListener` to
|
||||||
reinitialization ([#4510](https://github.com/google/ExoPlayer/issues/4510)).
|
be notified of acquired and released DRM sessions.
|
||||||
* Propagate the end-of-stream signal directly in the renderer when using
|
* Add uri field to `LoadEventInfo` in `MediaSourceEventListener` and
|
||||||
tunneling, to fix an issue where the player would remain ready after the
|
|
||||||
stream ended.
|
|
||||||
* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when
|
|
||||||
creating a `CacheDataSource`.
|
|
||||||
* Turned on Java 8 compiler support for the ExoPlayer library. Apps that depend
|
|
||||||
on ExoPlayer via its source code rather than an AAR may need to add
|
|
||||||
`compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their
|
|
||||||
gradle settings to ensure bytecode compatibility.
|
|
||||||
* ConcatenatingMediaSource:
|
|
||||||
* Add support for lazy preparation of playlist media sources
|
|
||||||
([#3972](https://github.com/google/ExoPlayer/issues/3972)).
|
|
||||||
* Add support for range removal with `removeMediaSourceRange` methods.
|
|
||||||
* `BandwidthMeter` management:
|
|
||||||
* Pass `BandwidthMeter` directly to `ExoPlayerFactory` instead of
|
|
||||||
`TrackSelection.Factory` and `DataSource.Factory`. May also be omitted to
|
|
||||||
use the default bandwidth meter automatically. This change only works
|
|
||||||
correctly if the following changes are adopted for custom `BandwidthMeter`s,
|
|
||||||
`TrackSelection`s, `MediaSource`s and `DataSource`s.
|
|
||||||
* Pass `BandwidthMeter` to `TrackSelection.Factory` which should be used to
|
|
||||||
obtain bandwidth estimates.
|
|
||||||
* Add method to `BandwidthMeter` to return the `TransferListener` used to
|
|
||||||
gather bandwidth information. Also add methods to add and remove event
|
|
||||||
listeners.
|
|
||||||
* Pass `TransferListener` to `MediaSource`s to listen to media data transfers.
|
|
||||||
* Add method to `DataSource` to add `TransferListener`s. Custom `DataSource`s
|
|
||||||
directly reading data should implement `BaseDataSource` to handle the
|
|
||||||
registration correctly. Custom `DataSource`'s forwarding to other sources
|
|
||||||
should forward all calls to `addTransferListener`.
|
|
||||||
* Extend `TransferListener` with additional callback parameters.
|
|
||||||
* Error handling:
|
|
||||||
* Allow configuration of the Loader retry delay
|
|
||||||
([#3370](https://github.com/google/ExoPlayer/issues/3370)).
|
|
||||||
* HLS:
|
|
||||||
* Add support for variable substitution
|
|
||||||
([#4422](https://github.com/google/ExoPlayer/issues/4422)).
|
|
||||||
* Add support for PlayReady.
|
|
||||||
* Add support for alternative EXT-X-KEY tags.
|
|
||||||
* Set the bitrate on primary track sample formats
|
|
||||||
([#3297](https://github.com/google/ExoPlayer/issues/3297)).
|
|
||||||
* Pass HTTP response headers to `HlsExtractorFactory.createExtractor`.
|
|
||||||
* Add support for EXT-X-INDEPENDENT-SEGMENTS in the master playlist.
|
|
||||||
* Support load error handling customization
|
|
||||||
([#2981](https://github.com/google/ExoPlayer/issues/2981)).
|
|
||||||
* Fix bug when reporting buffered position for multi-period windows and add
|
|
||||||
two additional convenience methods `Player.getTotalBufferedDuration` and
|
|
||||||
`Player.getContentBufferedDuration`
|
|
||||||
([#4023](https://github.com/google/ExoPlayer/issues/4023)).
|
|
||||||
* MediaSession extension:
|
|
||||||
* Allow apps to set custom metadata with a MediaMetadataProvider
|
|
||||||
([#3497](https://github.com/google/ExoPlayer/issues/3497)).
|
|
||||||
* Improved performance when playing high frame-rate content, and when playing
|
|
||||||
at greater than 1x speed
|
|
||||||
([#2777](https://github.com/google/ExoPlayer/issues/2777)).
|
|
||||||
* Allow setting the `Looper`, which is used to access the player, in
|
|
||||||
`ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)).
|
|
||||||
* Use default Deserializers if non given to DownloadManager.
|
|
||||||
* 360:
|
|
||||||
* Add monoscopic 360 surface type to PlayerView.
|
|
||||||
* Support
|
|
||||||
[VR180 video format](https://github.com/google/spatial-media/blob/master/docs/vr180.md).
|
|
||||||
* Deprecate `Player.DefaultEventListener` as selective listener overrides can
|
|
||||||
be directly made with the `Player.EventListener` interface.
|
|
||||||
* Deprecate `DefaultAnalyticsListener` as selective listener overrides can be
|
|
||||||
directly made with the `AnalyticsListener` interface.
|
|
||||||
* Add uri field to `LoadEventInfo` in `MediaSourceEventListener` or
|
|
||||||
`AnalyticsListener` callbacks. This uri is the redirected uri if redirection
|
`AnalyticsListener` callbacks. This uri is the redirected uri if redirection
|
||||||
occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)).
|
occurred ([#2054](https://github.com/google/ExoPlayer/issues/2054)).
|
||||||
* Add response headers field to `LoadEventInfo` in `MediaSourceEventListener` or
|
* Add response headers field to `LoadEventInfo` in `MediaSourceEventListener`
|
||||||
`AnalyticsListener` callbacks
|
and `AnalyticsListener` callbacks
|
||||||
([#4361](https://github.com/google/ExoPlayer/issues/4361) and
|
([#4361](https://github.com/google/ExoPlayer/issues/4361) and
|
||||||
[#4615](https://github.com/google/ExoPlayer/issues/4615)).
|
[#4615](https://github.com/google/ExoPlayer/issues/4615)).
|
||||||
|
* UI components:
|
||||||
|
* Add option to `PlayerView` to show buffering view when playWhenReady is
|
||||||
|
false ([#4304](https://github.com/google/ExoPlayer/issues/4304)).
|
||||||
|
* Allow any `Drawable` to be used as `PlayerView` default artwork.
|
||||||
|
* ConcatenatingMediaSource:
|
||||||
|
* Support lazy preparation of playlist media sources
|
||||||
|
([#3972](https://github.com/google/ExoPlayer/issues/3972)).
|
||||||
|
* Support range removal with `removeMediaSourceRange` methods
|
||||||
|
([#4542](https://github.com/google/ExoPlayer/issues/4542)).
|
||||||
|
* Support setting a new shuffle order with `setShuffleOrder`
|
||||||
|
([#4791](https://github.com/google/ExoPlayer/issues/4791)).
|
||||||
|
* MPEG-TS: Support CEA-608/708 in H262
|
||||||
|
([#2565](https://github.com/google/ExoPlayer/issues/2565)).
|
||||||
|
* Allow configuration of the back buffer in `DefaultLoadControl.Builder`
|
||||||
|
([#4857](https://github.com/google/ExoPlayer/issues/4857)).
|
||||||
|
* Allow apps to pass a `CacheKeyFactory` for setting custom cache keys when
|
||||||
|
creating a `CacheDataSource`.
|
||||||
|
* Provide additional information for adaptive track selection.
|
||||||
|
`TrackSelection.updateSelectedTrack` has two new parameters for the current
|
||||||
|
queue of media chunks and iterators for information about upcoming chunks.
|
||||||
* Allow `MediaCodecSelector`s to return multiple compatible decoders for
|
* Allow `MediaCodecSelector`s to return multiple compatible decoders for
|
||||||
`MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that
|
`MediaCodecRenderer`, and provide an (optional) `MediaCodecSelector` that
|
||||||
falls back to less preferred decoders like `MediaCodec.createDecoderByType`
|
falls back to less preferred decoders like `MediaCodec.createDecoderByType`
|
||||||
([#273](https://github.com/google/ExoPlayer/issues/273)).
|
([#273](https://github.com/google/ExoPlayer/issues/273)).
|
||||||
* Fix where transitions to clipped media sources happened too early
|
* Enable gzip for requests made by `SingleSampleMediaSource`
|
||||||
|
([#4771](https://github.com/google/ExoPlayer/issues/4771)).
|
||||||
|
* Fix bug reporting buffered position for multi-period windows, and add
|
||||||
|
convenience methods `Player.getTotalBufferedDuration` and
|
||||||
|
`Player.getContentBufferedDuration`
|
||||||
|
([#4023](https://github.com/google/ExoPlayer/issues/4023)).
|
||||||
|
* Fix bug where transitions to clipped media sources would happen too early
|
||||||
([#4583](https://github.com/google/ExoPlayer/issues/4583)).
|
([#4583](https://github.com/google/ExoPlayer/issues/4583)).
|
||||||
* Add `DataSpec.httpMethod` and update `HttpDataSource` implementations to
|
* Fix bugs reporting events for multi-period media sources
|
||||||
support HTTP HEAD method. Previously, only GET and POST were supported.
|
([#4492](https://github.com/google/ExoPlayer/issues/4492) and
|
||||||
* Add option to show buffering view when playWhenReady is false
|
[#4634](https://github.com/google/ExoPlayer/issues/4634)).
|
||||||
([#4304](https://github.com/google/ExoPlayer/issues/4304)).
|
* Fix issue where removing looping media from a playlist throws an exception
|
||||||
* Allow any `Drawable` to be used as `PlayerView` default artwork.
|
([#4871](https://github.com/google/ExoPlayer/issues/4871).
|
||||||
* IMA:
|
* Fix issue where the preferred audio or text track would not be selected if
|
||||||
|
mapped onto a secondary renderer of the corresponding type
|
||||||
|
([#4711](http://github.com/google/ExoPlayer/issues/4711)).
|
||||||
|
* Fix issue where errors of upcoming playlist items are thrown too early
|
||||||
|
([#4661](https://github.com/google/ExoPlayer/issues/4661)).
|
||||||
|
* Allow edit lists which do not start with a sync sample.
|
||||||
|
([#4774](https://github.com/google/ExoPlayer/issues/4774)).
|
||||||
|
* Fix issue with audio discontinuities at period transitions, e.g. when
|
||||||
|
looping ([#3829](https://github.com/google/ExoPlayer/issues/3829)).
|
||||||
|
* Fix issue where `player.getCurrentTag()` throws an `IndexOutOfBoundsException`
|
||||||
|
([#4822](https://github.com/google/ExoPlayer/issues/4822)).
|
||||||
|
* Fix bug preventing use of multiple key session support (`multiSession=true`)
|
||||||
|
for non-Widevine `DefaultDrmSessionManager` instances
|
||||||
|
([#4834](https://github.com/google/ExoPlayer/issues/4834)).
|
||||||
|
* Fix issue where audio and video would desynchronize when playing
|
||||||
|
concatenations of gapless content
|
||||||
|
([#4559](https://github.com/google/ExoPlayer/issues/4559)).
|
||||||
|
* IMA extension:
|
||||||
* Refine the previous fix for empty ad groups to avoid discarding ad breaks
|
* Refine the previous fix for empty ad groups to avoid discarding ad breaks
|
||||||
unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030)),
|
unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030) and
|
||||||
([#4280](https://github.com/google/ExoPlayer/issues/4280)).
|
[#4280](https://github.com/google/ExoPlayer/issues/4280)).
|
||||||
* Fix handling of empty postrolls
|
* Fix handling of empty postrolls
|
||||||
([#4681](https://github.com/google/ExoPlayer/issues/4681).
|
([#4681](https://github.com/google/ExoPlayer/issues/4681)).
|
||||||
* Fix handling of postrolls with multiple ads
|
* Fix handling of postrolls with multiple ads
|
||||||
([#4710](https://github.com/google/ExoPlayer/issues/4710).
|
([#4710](https://github.com/google/ExoPlayer/issues/4710)).
|
||||||
|
* MediaSession extension:
|
||||||
|
* Add `MediaSessionConnector.setCustomErrorMessage` to support setting custom
|
||||||
|
error messages.
|
||||||
|
* Add `MediaMetadataProvider` to support setting custom metadata
|
||||||
|
([#3497](https://github.com/google/ExoPlayer/issues/3497)).
|
||||||
|
* Cronet extension: Now distributed via jCenter.
|
||||||
|
* FFmpeg extension: Support mu-law and A-law PCM.
|
||||||
|
|
||||||
### 2.8.4 ###
|
### 2.8.4 ###
|
||||||
|
|
||||||
* IMA: Improve handling of consecutive empty ad groups
|
* IMA extension: Improve handling of consecutive empty ad groups
|
||||||
([#4030](https://github.com/google/ExoPlayer/issues/4030)),
|
([#4030](https://github.com/google/ExoPlayer/issues/4030)),
|
||||||
([#4280](https://github.com/google/ExoPlayer/issues/4280)).
|
([#4280](https://github.com/google/ExoPlayer/issues/4280)).
|
||||||
|
|
||||||
### 2.8.3 ###
|
### 2.8.3 ###
|
||||||
|
|
||||||
* IMA:
|
* IMA extension:
|
||||||
* Fix behavior when creating/releasing the player then releasing
|
* Fix behavior when creating/releasing the player then releasing
|
||||||
`ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)).
|
`ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)).
|
||||||
* Add support for setting slots for companion ads.
|
* Add support for setting slots for companion ads.
|
||||||
|
|
@ -182,8 +648,9 @@
|
||||||
|
|
||||||
### 2.8.2 ###
|
### 2.8.2 ###
|
||||||
|
|
||||||
* IMA: Don't advertise support for video/mpeg ad media, as we don't have an
|
* IMA extension: Don't advertise support for video/mpeg ad media, as we don't
|
||||||
extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)).
|
have an extractor for this
|
||||||
|
([#4297](https://github.com/google/ExoPlayer/issues/4297)).
|
||||||
* DASH: Fix playback getting stuck when playing representations that have both
|
* DASH: Fix playback getting stuck when playing representations that have both
|
||||||
sidx atoms and non-zero presentationTimeOffset values.
|
sidx atoms and non-zero presentationTimeOffset values.
|
||||||
* HLS:
|
* HLS:
|
||||||
|
|
@ -293,18 +760,18 @@
|
||||||
begins, and poll the audio timestamp less frequently once it starts
|
begins, and poll the audio timestamp less frequently once it starts
|
||||||
advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)).
|
advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)).
|
||||||
* Add an option to skip silent audio in `PlaybackParameters`
|
* Add an option to skip silent audio in `PlaybackParameters`
|
||||||
((#2635)[https://github.com/google/ExoPlayer/issues/2635]).
|
([#2635](https://github.com/google/ExoPlayer/issues/2635)).
|
||||||
* Fix an issue where playback of TrueHD streams would get stuck after seeking
|
* Fix an issue where playback of TrueHD streams would get stuck after seeking
|
||||||
due to not finding a syncframe
|
due to not finding a syncframe
|
||||||
((#3845)[https://github.com/google/ExoPlayer/issues/3845]).
|
([#3845](https://github.com/google/ExoPlayer/issues/3845)).
|
||||||
* Fix an issue with eac3-joc playback where a codec would fail to configure
|
* Fix an issue with eac3-joc playback where a codec would fail to configure
|
||||||
((#4165)[https://github.com/google/ExoPlayer/issues/4165]).
|
([#4165](https://github.com/google/ExoPlayer/issues/4165)).
|
||||||
* Handle non-empty end-of-stream buffers, to fix gapless playback of streams
|
* Handle non-empty end-of-stream buffers, to fix gapless playback of streams
|
||||||
with encoder padding when the decoder returns a non-empty final buffer.
|
with encoder padding when the decoder returns a non-empty final buffer.
|
||||||
* Allow trimming more than one sample when applying an elst audio edit via
|
* Allow trimming more than one sample when applying an elst audio edit via
|
||||||
gapless playback info.
|
gapless playback info.
|
||||||
* Allow overriding skipping/scaling with custom `AudioProcessor`s
|
* Allow overriding skipping/scaling with custom `AudioProcessor`s
|
||||||
((#3142)[https://github.com/google/ExoPlayer/issues/3142]).
|
([#3142](https://github.com/google/ExoPlayer/issues/3142)).
|
||||||
* Caching:
|
* Caching:
|
||||||
* Add release method to the `Cache` interface, and prevent multiple instances
|
* Add release method to the `Cache` interface, and prevent multiple instances
|
||||||
of `SimpleCache` using the same folder at the same time.
|
of `SimpleCache` using the same folder at the same time.
|
||||||
|
|
@ -328,7 +795,7 @@
|
||||||
([#4164](https://github.com/google/ExoPlayer/issues/4182)).
|
([#4164](https://github.com/google/ExoPlayer/issues/4182)).
|
||||||
* Fix seeking in live streams
|
* Fix seeking in live streams
|
||||||
([#4187](https://github.com/google/ExoPlayer/issues/4187)).
|
([#4187](https://github.com/google/ExoPlayer/issues/4187)).
|
||||||
* IMA:
|
* IMA extension:
|
||||||
* Allow setting the ad media load timeout
|
* Allow setting the ad media load timeout
|
||||||
([#3691](https://github.com/google/ExoPlayer/issues/3691)).
|
([#3691](https://github.com/google/ExoPlayer/issues/3691)).
|
||||||
* Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`,
|
* Expose ad load errors via `MediaSourceEventListener` on `AdsMediaSource`,
|
||||||
|
|
@ -1001,7 +1468,7 @@
|
||||||
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
|
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
|
||||||
* Robustness improvements when handling MediaSource timeline changes and
|
* Robustness improvements when handling MediaSource timeline changes and
|
||||||
MediaPeriod transitions.
|
MediaPeriod transitions.
|
||||||
* EIA608: Support for caption styling and positioning.
|
* CEA-608: Support for caption styling and positioning.
|
||||||
* MPEG-TS: Improved support:
|
* MPEG-TS: Improved support:
|
||||||
* Support injection of custom TS payload readers.
|
* Support injection of custom TS payload readers.
|
||||||
* Support injection of custom section payload readers.
|
* Support injection of custom section payload readers.
|
||||||
|
|
@ -1245,8 +1712,8 @@ V2 release.
|
||||||
(#801).
|
(#801).
|
||||||
* MP3: Fix playback of some streams when stream length is unknown.
|
* MP3: Fix playback of some streams when stream length is unknown.
|
||||||
* ID3: Support multiple frames of the same type in a single tag.
|
* ID3: Support multiple frames of the same type in a single tag.
|
||||||
* EIA608: Correctly handle repeated control characters, fixing an issue in which
|
* CEA-608: Correctly handle repeated control characters, fixing an issue in
|
||||||
captions would immediately disappear.
|
which captions would immediately disappear.
|
||||||
* AVC3: Fix decoder failures on some MediaTek devices in the case where the
|
* AVC3: Fix decoder failures on some MediaTek devices in the case where the
|
||||||
first buffer fed to the decoder does not start with SPS/PPS NAL units.
|
first buffer fed to the decoder does not start with SPS/PPS NAL units.
|
||||||
* Misc bug fixes.
|
* Misc bug fixes.
|
||||||
|
|
|
||||||
21
build.gradle
|
|
@ -13,30 +13,22 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
|
||||||
google()
|
google()
|
||||||
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.1.4'
|
classpath 'com.android.tools.build:gradle:3.4.0'
|
||||||
classpath 'com.novoda:bintray-release:0.8.1'
|
classpath 'com.novoda:bintray-release:0.9'
|
||||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3'
|
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 {
|
||||||
jcenter()
|
|
||||||
google()
|
google()
|
||||||
|
jcenter()
|
||||||
}
|
}
|
||||||
project.ext {
|
project.ext {
|
||||||
exoplayerPublishEnabled = true
|
exoplayerPublishEnabled = false
|
||||||
}
|
}
|
||||||
if (it.hasProperty('externalBuildDir')) {
|
if (it.hasProperty('externalBuildDir')) {
|
||||||
if (!new File(externalBuildDir).isAbsolute()) {
|
if (!new File(externalBuildDir).isAbsolute()) {
|
||||||
|
|
@ -44,6 +36,7 @@ allprojects {
|
||||||
}
|
}
|
||||||
buildDir = "${externalBuildDir}/${project.name}"
|
buildDir = "${externalBuildDir}/${project.name}"
|
||||||
}
|
}
|
||||||
|
group = 'com.google.android.exoplayer'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'javadoc_combined.gradle'
|
apply from: 'javadoc_combined.gradle'
|
||||||
|
|
|
||||||
|
|
@ -13,26 +13,20 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.8.4'
|
releaseVersion = '2.10.4'
|
||||||
releaseVersionCode = 2804
|
releaseVersionCode = 2010004
|
||||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
minSdkVersion = 16
|
||||||
// components provided by the library may be of use on older devices.
|
|
||||||
// However, please note that the core media playback functionality provided
|
|
||||||
// by the library requires API level 16 or greater.
|
|
||||||
minSdkVersion = 14
|
|
||||||
targetSdkVersion = 28
|
targetSdkVersion = 28
|
||||||
compileSdkVersion = 28
|
compileSdkVersion = 29
|
||||||
buildToolsVersion = '28.0.2'
|
dexmakerVersion = '2.21.0'
|
||||||
testSupportLibraryVersion = '0.5'
|
mockitoVersion = '2.25.0'
|
||||||
supportLibraryVersion = '27.1.1'
|
robolectricVersion = '4.3'
|
||||||
dexmakerVersion = '1.2'
|
|
||||||
mockitoVersion = '1.9.5'
|
|
||||||
junitVersion = '4.12'
|
|
||||||
truthVersion = '0.39'
|
|
||||||
robolectricVersion = '3.7.1'
|
|
||||||
autoValueVersion = '1.6'
|
autoValueVersion = '1.6'
|
||||||
|
autoServiceVersion = '1.0-rc4'
|
||||||
checkerframeworkVersion = '2.5.0'
|
checkerframeworkVersion = '2.5.0'
|
||||||
testRunnerVersion = '1.1.0-alpha3'
|
jsr305Version = '3.0.2'
|
||||||
|
androidXTestVersion = '1.1.0'
|
||||||
|
truthVersion = '0.44'
|
||||||
modulePrefix = ':'
|
modulePrefix = ':'
|
||||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
@ -26,7 +25,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,10 +44,9 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
// The demo app does not have translations.
|
// The demo app isn't indexed and doesn't have translations.
|
||||||
disable 'MissingTranslation'
|
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -58,9 +56,10 @@ dependencies {
|
||||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||||
implementation project(modulePrefix + 'library-ui')
|
implementation project(modulePrefix + 'library-ui')
|
||||||
implementation project(modulePrefix + 'extension-cast')
|
implementation project(modulePrefix + 'extension-cast')
|
||||||
implementation 'com.android.support:support-v4:' + supportLibraryVersion
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Proguard rules specific to the Cast demo app.
|
# Proguard rules specific to the Cast demo app.
|
||||||
|
|
||||||
# Accessed via menu.xml
|
# Accessed via menu.xml
|
||||||
-keep class android.support.v7.app.MediaRouteActionProvider {
|
-keep class androidx.mediarouter.app.MediaRouteActionProvider {
|
||||||
*;
|
*;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
package="com.google.android.exoplayer2.castdemo">
|
package="com.google.android.exoplayer2.castdemo">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
|
||||||
<uses-sdk/>
|
<uses-sdk/>
|
||||||
|
|
||||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,16 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.castdemo;
|
package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
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 com.google.android.gms.cast.MediaInfo;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/** 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 {
|
||||||
|
|
||||||
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
|
||||||
|
|
@ -31,62 +32,73 @@ import java.util.List;
|
||||||
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<MediaItem> SAMPLES;
|
||||||
*/
|
|
||||||
public static final List<Sample> SAMPLES;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a media sample.
|
|
||||||
*/
|
|
||||||
public static final class Sample {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The uri from which the media sample is obtained.
|
|
||||||
*/
|
|
||||||
public final String uri;
|
|
||||||
/**
|
|
||||||
* A descriptive name for the sample.
|
|
||||||
*/
|
|
||||||
public final String name;
|
|
||||||
/**
|
|
||||||
* The mime type of the media sample, as required by {@link MediaInfo#setContentType}.
|
|
||||||
*/
|
|
||||||
public final String mimeType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 = uri;
|
|
||||||
this.name = name;
|
|
||||||
this.mimeType = mimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// App samples.
|
ArrayList<MediaItem> samples = new ArrayList<>();
|
||||||
ArrayList<Sample> samples = new ArrayList<>();
|
|
||||||
samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
|
|
||||||
"DASH (clear,MP4,H264)", MIME_TYPE_DASH));
|
|
||||||
samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/"
|
|
||||||
+ "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS));
|
|
||||||
samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)",
|
|
||||||
MIME_TYPE_VIDEO_MP4));
|
|
||||||
|
|
||||||
|
// Clear content.
|
||||||
|
samples.add(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
|
||||||
|
.setTitle("Clear DASH: Tears")
|
||||||
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
|
.build());
|
||||||
|
samples.add(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
|
||||||
|
.setTitle("Clear HLS: Angel one")
|
||||||
|
.setMimeType(MIME_TYPE_HLS)
|
||||||
|
.build());
|
||||||
|
samples.add(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri("https://html5demos.com/assets/dizzy.mp4")
|
||||||
|
.setTitle("Clear MP4: Dizzy")
|
||||||
|
.setMimeType(MIME_TYPE_VIDEO_MP4)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// DRM content.
|
||||||
|
samples.add(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(Uri.parse("https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"))
|
||||||
|
.setTitle("Widevine DASH cenc: Tears")
|
||||||
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
|
.setDrmConfiguration(
|
||||||
|
new DrmConfiguration(
|
||||||
|
C.WIDEVINE_UUID,
|
||||||
|
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||||
|
Collections.emptyMap()))
|
||||||
|
.build());
|
||||||
|
samples.add(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(
|
||||||
|
Uri.parse(
|
||||||
|
"https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"))
|
||||||
|
.setTitle("Widevine DASH cbc1: Tears")
|
||||||
|
.setMimeType(MIME_TYPE_DASH)
|
||||||
|
.setDrmConfiguration(
|
||||||
|
new DrmConfiguration(
|
||||||
|
C.WIDEVINE_UUID,
|
||||||
|
Uri.parse("https://proxy.uat.widevine.com/proxy?provider=widevine_test"),
|
||||||
|
Collections.emptyMap()))
|
||||||
|
.build());
|
||||||
|
samples.add(
|
||||||
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DemoUtil() {}
|
private DemoUtil() {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,15 @@ package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v4.graphics.ColorUtils;
|
import androidx.annotation.NonNull;
|
||||||
import android.support.v7.app.AlertDialog;
|
import androidx.annotation.Nullable;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import androidx.core.graphics.ColorUtils;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
|
@ -33,20 +35,22 @@ import android.view.ViewGroup;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
import com.google.android.exoplayer2.ext.cast.MediaItem;
|
||||||
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
|
||||||
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.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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
|
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
|
||||||
|
* Cast extension.
|
||||||
*/
|
*/
|
||||||
public class MainActivity extends AppCompatActivity implements OnClickListener,
|
public class MainActivity extends AppCompatActivity
|
||||||
PlayerManager.QueuePositionListener {
|
implements OnClickListener, PlayerManager.Listener {
|
||||||
|
|
||||||
private PlayerView localPlayerView;
|
private PlayerView localPlayerView;
|
||||||
private PlayerControlView castControlView;
|
private PlayerControlView castControlView;
|
||||||
|
|
@ -61,7 +65,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
// Getting the cast context later than onStart can cause device discovery not to take place.
|
// Getting the cast context later than onStart can cause device discovery not to take place.
|
||||||
|
try {
|
||||||
castContext = CastContext.getSharedInstance(this);
|
castContext = CastContext.getSharedInstance(this);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
while (cause != null) {
|
||||||
|
if (cause instanceof DynamiteModule.LoadingException) {
|
||||||
|
setContentView(R.layout.cast_context_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cause = cause.getCause();
|
||||||
|
}
|
||||||
|
// Unknown error. We propagate it.
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
|
|
||||||
|
|
@ -91,9 +108,13 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
if (castContext == null) {
|
||||||
|
// There is no Cast context to work with. Do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
playerManager =
|
playerManager =
|
||||||
PlayerManager.createPlayerManager(
|
new PlayerManager(
|
||||||
/* queuePositionListener= */ this,
|
/* listener= */ this,
|
||||||
localPlayerView,
|
localPlayerView,
|
||||||
castControlView,
|
castControlView,
|
||||||
/* context= */ this,
|
/* context= */ this,
|
||||||
|
|
@ -104,9 +125,14 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
if (castContext == null) {
|
||||||
|
// Nothing to release.
|
||||||
|
return;
|
||||||
|
}
|
||||||
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
||||||
mediaQueueList.setAdapter(null);
|
mediaQueueList.setAdapter(null);
|
||||||
playerManager.release();
|
playerManager.release();
|
||||||
|
playerManager = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activity input.
|
// Activity input.
|
||||||
|
|
@ -119,12 +145,15 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title)
|
new AlertDialog.Builder(this)
|
||||||
.setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create()
|
.setTitle(R.string.add_samples)
|
||||||
|
.setView(buildSampleListView())
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.create()
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayerManager.QueuePositionListener implementation.
|
// PlayerManager.Listener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
public void onQueuePositionChanged(int previousIndex, int newIndex) {
|
||||||
|
|
@ -136,8 +165,23 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUnsupportedTrack(int trackType) {
|
||||||
|
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||||
|
showToast(R.string.error_unsupported_audio);
|
||||||
|
} else if (trackType == C.TRACK_TYPE_VIDEO) {
|
||||||
|
showToast(R.string.error_unsupported_video);
|
||||||
|
} else {
|
||||||
|
// 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);
|
||||||
|
|
@ -152,23 +196,6 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
|
|
||||||
// Internal classes.
|
// Internal classes.
|
||||||
|
|
||||||
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
|
||||||
|
|
||||||
public final TextView textView;
|
|
||||||
|
|
||||||
public QueueItemViewHolder(TextView textView) {
|
|
||||||
super(textView);
|
|
||||||
this.textView = textView;
|
|
||||||
textView.setOnClickListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
playerManager.selectQueueItem(getAdapterPosition());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
private class MediaQueueListAdapter extends RecyclerView.Adapter<QueueItemViewHolder> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -180,10 +207,13 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
public void onBindViewHolder(QueueItemViewHolder holder, int position) {
|
||||||
|
holder.item = playerManager.getItem(position);
|
||||||
TextView view = holder.textView;
|
TextView view = holder.textView;
|
||||||
view.setText(playerManager.getItem(position).name);
|
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(
|
||||||
|
ColorUtils.setAlphaComponent(
|
||||||
|
view.getCurrentTextColor(),
|
||||||
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
position == playerManager.getCurrentItemIndex() ? 255 : 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,8 +252,11 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
@Override
|
@Override
|
||||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||||
int position = viewHolder.getAdapterPosition();
|
int position = viewHolder.getAdapterPosition();
|
||||||
if (playerManager.removeItem(position)) {
|
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||||
|
if (playerManager.removeItem(queueItemHolder.item)) {
|
||||||
mediaQueueListAdapter.notifyItemRemoved(position);
|
mediaQueueListAdapter.notifyItemRemoved(position);
|
||||||
|
// Update whichever item took its place, in case it became the new selected item.
|
||||||
|
mediaQueueListAdapter.notifyItemChanged(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,8 +264,9 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
|
||||||
super.clearView(recyclerView, viewHolder);
|
super.clearView(recyclerView, viewHolder);
|
||||||
if (draggingFromPosition != C.INDEX_UNSET) {
|
if (draggingFromPosition != C.INDEX_UNSET) {
|
||||||
|
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
|
||||||
// A drag has ended. We reflect the media queue change in the player.
|
// A drag has ended. We reflect the media queue change in the player.
|
||||||
if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) {
|
if (!playerManager.moveItem(queueItemHolder.item, draggingToPosition)) {
|
||||||
// The move failed. The entire sequence of onMove calls since the drag started needs to be
|
// The move failed. The entire sequence of onMove calls since the drag started needs to be
|
||||||
// invalidated.
|
// invalidated.
|
||||||
mediaQueueListAdapter.notifyDataSetChanged();
|
mediaQueueListAdapter.notifyDataSetChanged();
|
||||||
|
|
@ -241,15 +275,37 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
||||||
draggingFromPosition = C.INDEX_UNSET;
|
draggingFromPosition = C.INDEX_UNSET;
|
||||||
draggingToPosition = C.INDEX_UNSET;
|
draggingToPosition = C.INDEX_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SampleListAdapter extends ArrayAdapter<Sample> {
|
private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
|
||||||
|
|
||||||
|
public final TextView textView;
|
||||||
|
public MediaItem item;
|
||||||
|
|
||||||
|
public QueueItemViewHolder(TextView textView) {
|
||||||
|
super(textView);
|
||||||
|
this.textView = textView;
|
||||||
|
textView.setOnClickListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
playerManager.selectQueueItem(getAdapterPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -17,52 +17,62 @@ package com.google.android.exoplayer2.castdemo;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||||
import com.google.android.exoplayer2.Player.EventListener;
|
import com.google.android.exoplayer2.Player.EventListener;
|
||||||
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
import com.google.android.exoplayer2.Player.TimelineChangeReason;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.Timeline.Period;
|
import com.google.android.exoplayer2.Timeline.Period;
|
||||||
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
|
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.CastPlayer;
|
||||||
|
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
|
||||||
|
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.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
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.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
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.MediaQueueItem;
|
||||||
import com.google.android.gms.cast.framework.CastContext;
|
import com.google.android.gms.cast.framework.CastContext;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
|
/** Manages players and an internal media queue for the demo app. */
|
||||||
/* package */ final class PlayerManager
|
/* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
|
||||||
implements EventListener, CastPlayer.SessionAvailabilityListener {
|
|
||||||
|
|
||||||
/**
|
/** Listener for events. */
|
||||||
* Listener for changes in the media queue playback position.
|
interface Listener {
|
||||||
*/
|
|
||||||
public interface QueuePositionListener {
|
|
||||||
|
|
||||||
/**
|
/** 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 a track of type {@code trackType} is not supported by the player.
|
||||||
|
*
|
||||||
|
* @param trackType One of the {@link C}{@code .TRACK_TYPE_*} constants.
|
||||||
|
*/
|
||||||
|
void onUnsupportedTrack(int trackType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
private static final String USER_AGENT = "ExoCastDemoPlayer";
|
||||||
|
|
@ -71,52 +81,45 @@ import java.util.ArrayList;
|
||||||
|
|
||||||
private final PlayerView localPlayerView;
|
private final PlayerView localPlayerView;
|
||||||
private final PlayerControlView castControlView;
|
private final PlayerControlView castControlView;
|
||||||
|
private final DefaultTrackSelector trackSelector;
|
||||||
private final SimpleExoPlayer exoPlayer;
|
private final SimpleExoPlayer exoPlayer;
|
||||||
private final CastPlayer castPlayer;
|
private final CastPlayer castPlayer;
|
||||||
private final ArrayList<DemoUtil.Sample> mediaQueue;
|
private final ArrayList<MediaItem> mediaQueue;
|
||||||
private final QueuePositionListener queuePositionListener;
|
private final Listener listener;
|
||||||
private final ConcatenatingMediaSource concatenatingMediaSource;
|
private final ConcatenatingMediaSource concatenatingMediaSource;
|
||||||
|
private final MediaItemConverter mediaItemConverter;
|
||||||
|
private final IdentityHashMap<MediaSource, FrameworkMediaDrm> mediaDrms;
|
||||||
|
|
||||||
private boolean castMediaQueueCreationPending;
|
private TrackGroupArray lastSeenTrackGroupArray;
|
||||||
private int currentItemIndex;
|
private int currentItemIndex;
|
||||||
private Player currentPlayer;
|
private Player currentPlayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
|
* 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 localPlayerView The {@link PlayerView} for local playback.
|
||||||
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
* @param castControlView The {@link PlayerControlView} to control remote playback.
|
||||||
* @param context A {@link Context}.
|
* @param context A {@link Context}.
|
||||||
* @param castContext The {@link CastContext}.
|
* @param castContext The {@link CastContext}.
|
||||||
*/
|
*/
|
||||||
public static PlayerManager createPlayerManager(
|
public PlayerManager(
|
||||||
QueuePositionListener queuePositionListener,
|
Listener listener,
|
||||||
PlayerView localPlayerView,
|
PlayerView localPlayerView,
|
||||||
PlayerControlView castControlView,
|
PlayerControlView castControlView,
|
||||||
Context context,
|
Context context,
|
||||||
CastContext castContext) {
|
CastContext castContext) {
|
||||||
PlayerManager playerManager =
|
this.listener = listener;
|
||||||
new PlayerManager(
|
|
||||||
queuePositionListener, localPlayerView, castControlView, context, castContext);
|
|
||||||
playerManager.init();
|
|
||||||
return playerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PlayerManager(
|
|
||||||
QueuePositionListener queuePositionListener,
|
|
||||||
PlayerView localPlayerView,
|
|
||||||
PlayerControlView castControlView,
|
|
||||||
Context context,
|
|
||||||
CastContext castContext) {
|
|
||||||
this.queuePositionListener = queuePositionListener;
|
|
||||||
this.localPlayerView = localPlayerView;
|
this.localPlayerView = localPlayerView;
|
||||||
this.castControlView = castControlView;
|
this.castControlView = castControlView;
|
||||||
mediaQueue = new ArrayList<>();
|
mediaQueue = new ArrayList<>();
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
concatenatingMediaSource = new ConcatenatingMediaSource();
|
concatenatingMediaSource = new ConcatenatingMediaSource();
|
||||||
|
mediaItemConverter = new DefaultMediaItemConverter();
|
||||||
|
mediaDrms = new IdentityHashMap<>();
|
||||||
|
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
|
trackSelector = new DefaultTrackSelector(context);
|
||||||
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
|
|
||||||
exoPlayer.addListener(this);
|
exoPlayer.addListener(this);
|
||||||
localPlayerView.setPlayer(exoPlayer);
|
localPlayerView.setPlayer(exoPlayer);
|
||||||
|
|
||||||
|
|
@ -124,6 +127,8 @@ import java.util.ArrayList;
|
||||||
castPlayer.addListener(this);
|
castPlayer.addListener(this);
|
||||||
castPlayer.setSessionAvailabilityListener(this);
|
castPlayer.setSessionAvailabilityListener(this);
|
||||||
castControlView.setPlayer(castPlayer);
|
castControlView.setPlayer(castPlayer);
|
||||||
|
|
||||||
|
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue manipulation methods.
|
// Queue manipulation methods.
|
||||||
|
|
@ -137,29 +142,25 @@ import java.util.ArrayList;
|
||||||
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
setCurrentItem(itemIndex, C.TIME_UNSET, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the index of the currently played item. */
|
||||||
* Returns the index of the currently played item.
|
|
||||||
*/
|
|
||||||
public int getCurrentItemIndex() {
|
public int getCurrentItemIndex() {
|
||||||
return currentItemIndex;
|
return currentItemIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends {@code sample} to the media queue.
|
* Appends {@code item} to the media queue.
|
||||||
*
|
*
|
||||||
* @param sample The {@link Sample} to append.
|
* @param item The {@link MediaItem} to append.
|
||||||
*/
|
*/
|
||||||
public void addItem(Sample sample) {
|
public void addItem(MediaItem item) {
|
||||||
mediaQueue.add(sample);
|
mediaQueue.add(item);
|
||||||
concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
|
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
|
||||||
if (currentPlayer == castPlayer) {
|
if (currentPlayer == castPlayer) {
|
||||||
castPlayer.addItems(buildMediaQueueItem(sample));
|
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns the size of the media queue. */
|
||||||
* Returns the size of the media queue.
|
|
||||||
*/
|
|
||||||
public int getMediaQueueSize() {
|
public int getMediaQueueSize() {
|
||||||
return mediaQueue.size();
|
return mediaQueue.size();
|
||||||
}
|
}
|
||||||
|
|
@ -170,18 +171,23 @@ import java.util.ArrayList;
|
||||||
* @param position The index of the item.
|
* @param position The index of the item.
|
||||||
* @return The item at the given index in the media queue.
|
* @return The item at the given index in the media queue.
|
||||||
*/
|
*/
|
||||||
public Sample getItem(int position) {
|
public MediaItem getItem(int position) {
|
||||||
return mediaQueue.get(position);
|
return mediaQueue.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the item at the given index from the media queue.
|
* Removes the item at the given index from the media queue.
|
||||||
*
|
*
|
||||||
* @param itemIndex The index of the item to remove.
|
* @param item The item to remove.
|
||||||
* @return Whether the removal was successful.
|
* @return Whether the removal was successful.
|
||||||
*/
|
*/
|
||||||
public boolean removeItem(int itemIndex) {
|
public boolean removeItem(MediaItem item) {
|
||||||
concatenatingMediaSource.removeMediaSource(itemIndex);
|
int itemIndex = mediaQueue.indexOf(item);
|
||||||
|
if (itemIndex == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MediaSource removedMediaSource = concatenatingMediaSource.removeMediaSource(itemIndex);
|
||||||
|
releaseMediaDrmOfMediaSource(removedMediaSource);
|
||||||
if (currentPlayer == castPlayer) {
|
if (currentPlayer == castPlayer) {
|
||||||
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
Timeline castTimeline = castPlayer.getCurrentTimeline();
|
||||||
|
|
@ -203,11 +209,15 @@ import java.util.ArrayList;
|
||||||
/**
|
/**
|
||||||
* Moves an item within the queue.
|
* Moves an item within the queue.
|
||||||
*
|
*
|
||||||
* @param fromIndex The index of the item to move.
|
* @param item The item to move.
|
||||||
* @param toIndex The target index of the item in the queue.
|
* @param toIndex The target index of the item in the queue.
|
||||||
* @return Whether the item move was successful.
|
* @return Whether the item move was successful.
|
||||||
*/
|
*/
|
||||||
public boolean moveItem(int fromIndex, int toIndex) {
|
public boolean moveItem(MediaItem item, int toIndex) {
|
||||||
|
int fromIndex = mediaQueue.indexOf(item);
|
||||||
|
if (fromIndex == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Player update.
|
// Player update.
|
||||||
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
|
||||||
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
|
||||||
|
|
@ -234,8 +244,6 @@ import java.util.ArrayList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Miscellaneous methods.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
|
||||||
*
|
*
|
||||||
|
|
@ -250,13 +258,14 @@ import java.util.ArrayList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Releases the manager and the players that it holds. */
|
||||||
* Releases the manager and the players that it holds.
|
|
||||||
*/
|
|
||||||
public void release() {
|
public void release() {
|
||||||
currentItemIndex = C.INDEX_UNSET;
|
currentItemIndex = C.INDEX_UNSET;
|
||||||
mediaQueue.clear();
|
mediaQueue.clear();
|
||||||
concatenatingMediaSource.clear();
|
concatenatingMediaSource.clear();
|
||||||
|
for (FrameworkMediaDrm mediaDrm : mediaDrms.values()) {
|
||||||
|
mediaDrm.release();
|
||||||
|
}
|
||||||
castPlayer.setSessionAvailabilityListener(null);
|
castPlayer.setSessionAvailabilityListener(null);
|
||||||
castPlayer.release();
|
castPlayer.release();
|
||||||
localPlayerView.setPlayer(null);
|
localPlayerView.setPlayer(null);
|
||||||
|
|
@ -266,7 +275,7 @@ import java.util.ArrayList;
|
||||||
// Player.EventListener implementation.
|
// Player.EventListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||||
updateCurrentItemIndex();
|
updateCurrentItemIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,11 +285,26 @@ import java.util.ArrayList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(
|
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
|
||||||
updateCurrentItemIndex();
|
updateCurrentItemIndex();
|
||||||
if (timeline.isEmpty()) {
|
}
|
||||||
castMediaQueueCreationPending = true;
|
|
||||||
|
@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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,15 +322,12 @@ import java.util.ArrayList;
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private void init() {
|
|
||||||
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCurrentItemIndex() {
|
private void updateCurrentItemIndex() {
|
||||||
int playbackState = currentPlayer.getPlaybackState();
|
int playbackState = currentPlayer.getPlaybackState();
|
||||||
maybeSetCurrentItemAndNotify(
|
maybeSetCurrentItemAndNotify(
|
||||||
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
|
||||||
? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET);
|
? currentPlayer.getCurrentWindowIndex()
|
||||||
|
: C.INDEX_UNSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCurrentPlayer(Player currentPlayer) {
|
private void setCurrentPlayer(Player currentPlayer) {
|
||||||
|
|
@ -327,26 +348,26 @@ import java.util.ArrayList;
|
||||||
long playbackPositionMs = C.TIME_UNSET;
|
long playbackPositionMs = C.TIME_UNSET;
|
||||||
int windowIndex = C.INDEX_UNSET;
|
int windowIndex = C.INDEX_UNSET;
|
||||||
boolean playWhenReady = false;
|
boolean playWhenReady = false;
|
||||||
if (this.currentPlayer != null) {
|
|
||||||
int playbackState = this.currentPlayer.getPlaybackState();
|
Player previousPlayer = this.currentPlayer;
|
||||||
|
if (previousPlayer != null) {
|
||||||
|
// Save state from the previous player.
|
||||||
|
int playbackState = previousPlayer.getPlaybackState();
|
||||||
if (playbackState != Player.STATE_ENDED) {
|
if (playbackState != Player.STATE_ENDED) {
|
||||||
playbackPositionMs = this.currentPlayer.getCurrentPosition();
|
playbackPositionMs = previousPlayer.getCurrentPosition();
|
||||||
playWhenReady = this.currentPlayer.getPlayWhenReady();
|
playWhenReady = previousPlayer.getPlayWhenReady();
|
||||||
windowIndex = this.currentPlayer.getCurrentWindowIndex();
|
windowIndex = previousPlayer.getCurrentWindowIndex();
|
||||||
if (windowIndex != currentItemIndex) {
|
if (windowIndex != currentItemIndex) {
|
||||||
playbackPositionMs = C.TIME_UNSET;
|
playbackPositionMs = C.TIME_UNSET;
|
||||||
windowIndex = currentItemIndex;
|
windowIndex = currentItemIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.currentPlayer.stop(true);
|
previousPlayer.stop(true);
|
||||||
} else {
|
|
||||||
// This is the initial setup. No need to save any state.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentPlayer = currentPlayer;
|
this.currentPlayer = currentPlayer;
|
||||||
|
|
||||||
// Media queue management.
|
// Media queue management.
|
||||||
castMediaQueueCreationPending = currentPlayer == castPlayer;
|
|
||||||
if (currentPlayer == exoPlayer) {
|
if (currentPlayer == exoPlayer) {
|
||||||
exoPlayer.prepare(concatenatingMediaSource);
|
exoPlayer.prepare(concatenatingMediaSource);
|
||||||
}
|
}
|
||||||
|
|
@ -366,12 +387,11 @@ import java.util.ArrayList;
|
||||||
*/
|
*/
|
||||||
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
|
||||||
maybeSetCurrentItemAndNotify(itemIndex);
|
maybeSetCurrentItemAndNotify(itemIndex);
|
||||||
if (castMediaQueueCreationPending) {
|
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
|
||||||
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
|
||||||
for (int i = 0; i < items.length; i++) {
|
for (int i = 0; i < items.length; i++) {
|
||||||
items[i] = buildMediaQueueItem(mediaQueue.get(i));
|
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
|
||||||
}
|
}
|
||||||
castMediaQueueCreationPending = false;
|
|
||||||
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
|
||||||
} else {
|
} else {
|
||||||
currentPlayer.seekTo(itemIndex, positionMs);
|
currentPlayer.seekTo(itemIndex, positionMs);
|
||||||
|
|
@ -383,34 +403,82 @@ import java.util.ArrayList;
|
||||||
if (this.currentItemIndex != currentItemIndex) {
|
if (this.currentItemIndex != currentItemIndex) {
|
||||||
int oldIndex = this.currentItemIndex;
|
int oldIndex = this.currentItemIndex;
|
||||||
this.currentItemIndex = currentItemIndex;
|
this.currentItemIndex = currentItemIndex;
|
||||||
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(DemoUtil.Sample sample) {
|
private MediaSource buildMediaSource(MediaItem item) {
|
||||||
Uri uri = Uri.parse(sample.uri);
|
Uri uri = item.uri;
|
||||||
switch (sample.mimeType) {
|
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:
|
case DemoUtil.MIME_TYPE_SS:
|
||||||
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
createdMediaSource =
|
||||||
|
new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
case DemoUtil.MIME_TYPE_DASH:
|
case DemoUtil.MIME_TYPE_DASH:
|
||||||
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
createdMediaSource =
|
||||||
|
new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
case DemoUtil.MIME_TYPE_HLS:
|
case DemoUtil.MIME_TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
createdMediaSource =
|
||||||
|
new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
case DemoUtil.MIME_TYPE_VIDEO_MP4:
|
||||||
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
|
createdMediaSource =
|
||||||
default: {
|
new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
|
||||||
throw new IllegalStateException("Unsupported type: " + sample.mimeType);
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
|
||||||
}
|
}
|
||||||
|
if (mediaDrm != null) {
|
||||||
|
mediaDrms.put(createdMediaSource, mediaDrm);
|
||||||
}
|
}
|
||||||
|
return createdMediaSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) {
|
private void releaseMediaDrmOfMediaSource(MediaSource mediaSource) {
|
||||||
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
|
FrameworkMediaDrm mediaDrmToRelease = mediaDrms.remove(mediaSource);
|
||||||
movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name);
|
if (mediaDrmToRelease != null) {
|
||||||
MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri)
|
mediaDrmToRelease.release();
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType)
|
}
|
||||||
.setMetadata(movieMetadata).build();
|
|
||||||
return new MediaQueueItem.Builder(mediaInfo).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,12 @@
|
||||||
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.
|
||||||
-->
|
-->
|
||||||
<vector android:alpha="0.8" android:height="24dp" android:viewportHeight="24.0"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportWidth="24.0" android:width="24dp"
|
android:height="24.0dp"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
android:viewportHeight="24.0"
|
||||||
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
|
android:viewportWidth="24.0"
|
||||||
|
android:width="24.0dp" >
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1h0c-0.55,0 -1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1v0c0,-0.55 0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1h0c0.55,0 1,0.45 1,1v5h5c0.55,0 1,0.45 1,1v0C19,12.55 18.55,13 18,13z"/>
|
||||||
</vector>
|
</vector>
|
||||||
22
demos/cast/src/main/res/layout/cast_context_error.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:text="@string/cast_context_error"/>
|
||||||
|
|
@ -19,34 +19,42 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:keepScreenOn="true">
|
android:keepScreenOn="true">
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
|
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="12"
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/black"
|
||||||
app:repeat_toggle_modes="all|one"/>
|
app:repeat_toggle_modes="all|one"/>
|
||||||
|
|
||||||
<RelativeLayout android:layout_width="match_parent"
|
<RelativeLayout android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="12">
|
android:layout_weight="1">
|
||||||
<android.support.v7.widget.RecyclerView android:id="@+id/sample_list"
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView android:id="@+id/sample_list"
|
||||||
android:choiceMode="singleChoice"
|
android:choiceMode="singleChoice"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
android:fadeScrollbars="false"/>
|
android:fadeScrollbars="false"/>
|
||||||
<ImageButton android:id="@+id/add_sample_button"
|
|
||||||
android:background="@drawable/ic_add_circle_white_24dp"
|
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/add_sample_button"
|
||||||
|
android:src="@drawable/ic_plus"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_alignParentRight="true"
|
android:layout_alignParentRight="true"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:padding="30dp"/>
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="@string/add_samples"/>
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
|
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="2"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:repeat_toggle_modes="all|one"
|
app:repeat_toggle_modes="all|one"
|
||||||
app:show_timeout="-1"/>
|
app:show_timeout="-1"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ListView android:id="@+id/sample_list"
|
<ListView android:id="@+id/sample_list"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/media_route_menu_item"
|
android:id="@+id/media_route_menu_item"
|
||||||
android:title="@string/media_route_menu_title"
|
android:title="@string/media_route_menu_title"
|
||||||
app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@
|
||||||
|
|
||||||
<string name="media_route_menu_title">Cast</string>
|
<string name="media_route_menu_title">Cast</string>
|
||||||
|
|
||||||
<string name="sample_list_dialog_title">Add samples</string>
|
<string name="add_samples">Add samples</string>
|
||||||
|
|
||||||
|
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
|
||||||
|
|
||||||
|
<string name="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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
demos/gvr/src/main/res/layout/sample_chooser_activity.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Copyright (C) 2016 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ListView android:id="@+id/sample_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
BIN
demos/gvr/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/gvr/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/gvr/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/gvr/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/gvr/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
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>
|
||||||
|
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
@ -26,7 +25,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,8 +41,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
// The demo app does not have translations.
|
// The demo app isn't indexed and doesn't have translations.
|
||||||
disable 'MissingTranslation'
|
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,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 'com.android.support:support-annotations:' + supportLibraryVersion
|
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'
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package="com.google.android.exoplayer2.imademo">
|
package="com.google.android.exoplayer2.imademo">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-sdk/>
|
<uses-sdk/>
|
||||||
|
|
||||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||||
|
|
|
||||||
|
|
@ -23,23 +23,20 @@ import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
|
||||||
import com.google.android.exoplayer2.ui.PlayerView;
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
|
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
|
||||||
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
|
/* package */ final class PlayerManager implements MediaSourceFactory {
|
||||||
|
|
||||||
private final ImaAdsLoader adsLoader;
|
private final ImaAdsLoader adsLoader;
|
||||||
private final DataSource.Factory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
|
|
@ -56,14 +53,9 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(Context context, PlayerView playerView) {
|
public void init(Context context, PlayerView playerView) {
|
||||||
// Create a default track selector.
|
|
||||||
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
|
|
||||||
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
|
||||||
|
|
||||||
// Create a player instance.
|
// Create a player instance.
|
||||||
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
|
player = ExoPlayerFactory.newSimpleInstance(context);
|
||||||
|
adsLoader.setPlayer(player);
|
||||||
// Bind the player to the view.
|
|
||||||
playerView.setPlayer(player);
|
playerView.setPlayer(player);
|
||||||
|
|
||||||
// This is the MediaSource representing the content media (i.e. not the ad).
|
// This is the MediaSource representing the content media (i.e. not the ad).
|
||||||
|
|
@ -73,10 +65,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
// Compose the content media source into a new AdsMediaSource with both ads and content.
|
||||||
MediaSource mediaSourceWithAds =
|
MediaSource mediaSourceWithAds =
|
||||||
new AdsMediaSource(
|
new AdsMediaSource(
|
||||||
contentMediaSource,
|
contentMediaSource, /* adMediaSourceFactory= */ this, adsLoader, playerView);
|
||||||
/* adMediaSourceFactory= */ this,
|
|
||||||
adsLoader,
|
|
||||||
playerView.getOverlayFrameLayout());
|
|
||||||
|
|
||||||
// Prepare the player with the source.
|
// Prepare the player with the source.
|
||||||
player.seekTo(contentPosition);
|
player.seekTo(contentPosition);
|
||||||
|
|
@ -89,6 +78,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
contentPosition = player.getContentPosition();
|
contentPosition = player.getContentPosition();
|
||||||
player.release();
|
player.release();
|
||||||
player = null;
|
player = null;
|
||||||
|
adsLoader.setPlayer(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +90,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
adsLoader.release();
|
adsLoader.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdsMediaSource.MediaSourceFactory implementation.
|
// MediaSourceFactory implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaSource createMediaSource(Uri uri) {
|
public MediaSource createMediaSource(Uri uri) {
|
||||||
|
|
@ -125,7 +115,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
<string name="application_name">Exo IMA Demo</string>
|
<string name="application_name">Exo IMA Demo</string>
|
||||||
|
|
||||||
<string name="content_url"><![CDATA[http://rmcdn.2mdn.net/MotifFiles/html/1248596/android_1330378998288.mp4]]></string>
|
<string name="content_url"><![CDATA[https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv]]></string>
|
||||||
|
|
||||||
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
|
<string name="ad_tag_url"><![CDATA[https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=]]></string>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
@ -26,7 +25,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionName project.ext.releaseVersion
|
versionName project.ext.releaseVersion
|
||||||
versionCode project.ext.releaseVersionCode
|
versionCode project.ext.releaseVersionCode
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,8 +44,9 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
// The demo app does not have translations.
|
// The demo app isn't indexed, doesn't have translations, and has a
|
||||||
disable 'MissingTranslation'
|
// banner for AndroidTV that's only in xhdpi density.
|
||||||
|
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "extensions"
|
flavorDimensions "extensions"
|
||||||
|
|
@ -62,7 +62,10 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
|
implementation 'androidx.viewpager:viewpager:1.0.0'
|
||||||
|
implementation 'androidx.fragment:fragment:1.0.0'
|
||||||
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation project(modulePrefix + 'library-dash')
|
implementation project(modulePrefix + 'library-dash')
|
||||||
implementation project(modulePrefix + 'library-hls')
|
implementation project(modulePrefix + 'library-hls')
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.google.android.exoplayer2.demo">
|
package="com.google.android.exoplayer2.demo">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
@ -33,11 +34,13 @@
|
||||||
android:banner="@drawable/ic_banner"
|
android:banner="@drawable/ic_banner"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:name="com.google.android.exoplayer2.demo.DemoApplication">
|
android:name="com.google.android.exoplayer2.demo.DemoApplication"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:label="@string/application_name">
|
android:label="@string/application_name"
|
||||||
|
android:theme="@style/Theme.AppCompat">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
|
|
||||||
|
|
@ -330,11 +330,11 @@
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Super speed",
|
"name": "Super speed",
|
||||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism"
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Super speed (PlayReady)",
|
"name": "Super speed (PlayReady)",
|
||||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism",
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
|
||||||
"drm_scheme": "playready"
|
"drm_scheme": "playready"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -352,11 +352,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple master playlist advanced (TS)",
|
"name": "Apple master playlist advanced (TS)",
|
||||||
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_example_v2/master.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple master playlist advanced (fMP4)",
|
"name": "Apple master playlist advanced (fMP4)",
|
||||||
"uri": "https://tungsten.aaplimg.com/VOD/bipbop_adv_fmp4_example/master.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple TS media playlist",
|
"name": "Apple TS media playlist",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
|
@ -28,21 +35,24 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
* Placeholder application to facilitate overriding Application methods for debugging and testing.
|
||||||
*/
|
*/
|
||||||
public class DemoApplication extends Application {
|
public class DemoApplication extends Application {
|
||||||
|
|
||||||
|
private static final String TAG = "DemoApplication";
|
||||||
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
private static final String DOWNLOAD_ACTION_FILE = "actions";
|
||||||
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
|
||||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||||
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
|
|
||||||
|
|
||||||
protected String userAgent;
|
protected String userAgent;
|
||||||
|
|
||||||
|
private DatabaseProvider databaseProvider;
|
||||||
private File downloadDirectory;
|
private File downloadDirectory;
|
||||||
private Cache downloadCache;
|
private Cache downloadCache;
|
||||||
private DownloadManager downloadManager;
|
private DownloadManager downloadManager;
|
||||||
|
|
@ -71,6 +81,18 @@ public class DemoApplication extends Application {
|
||||||
return "withExtensions".equals(BuildConfig.FLAVOR);
|
return "withExtensions".equals(BuildConfig.FLAVOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
|
||||||
|
@DefaultRenderersFactory.ExtensionRendererMode
|
||||||
|
int extensionRendererMode =
|
||||||
|
useExtensionRenderers()
|
||||||
|
? (preferExtensionRenderer
|
||||||
|
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||||
|
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||||
|
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||||
|
return new DefaultRenderersFactory(/* context= */ this)
|
||||||
|
.setExtensionRendererMode(extensionRendererMode);
|
||||||
|
}
|
||||||
|
|
||||||
public DownloadManager getDownloadManager() {
|
public DownloadManager getDownloadManager() {
|
||||||
initDownloadManager();
|
initDownloadManager();
|
||||||
return downloadManager;
|
return downloadManager;
|
||||||
|
|
@ -81,31 +103,51 @@ public class DemoApplication extends Application {
|
||||||
return downloadTracker;
|
return downloadTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected synchronized Cache getDownloadCache() {
|
||||||
|
if (downloadCache == null) {
|
||||||
|
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
||||||
|
downloadCache =
|
||||||
|
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
|
||||||
|
}
|
||||||
|
return downloadCache;
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized void initDownloadManager() {
|
private synchronized void initDownloadManager() {
|
||||||
if (downloadManager == null) {
|
if (downloadManager == null) {
|
||||||
|
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
|
||||||
|
upgradeActionFile(
|
||||||
|
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
|
||||||
|
upgradeActionFile(
|
||||||
|
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
|
||||||
DownloaderConstructorHelper downloaderConstructorHelper =
|
DownloaderConstructorHelper downloaderConstructorHelper =
|
||||||
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
|
||||||
downloadManager =
|
downloadManager =
|
||||||
new DownloadManager(
|
new DownloadManager(
|
||||||
downloaderConstructorHelper,
|
this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
|
||||||
MAX_SIMULTANEOUS_DOWNLOADS,
|
|
||||||
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
|
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE));
|
|
||||||
downloadTracker =
|
downloadTracker =
|
||||||
new DownloadTracker(
|
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
|
||||||
/* context= */ this,
|
|
||||||
buildDataSourceFactory(),
|
|
||||||
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
|
|
||||||
downloadManager.addListener(downloadTracker);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized Cache getDownloadCache() {
|
private void upgradeActionFile(
|
||||||
if (downloadCache == null) {
|
String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
|
||||||
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
|
try {
|
||||||
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());
|
ActionFileUpgradeUtil.upgradeAndDelete(
|
||||||
|
new File(getDownloadDirectory(), fileName),
|
||||||
|
/* downloadIdProvider= */ null,
|
||||||
|
downloadIndex,
|
||||||
|
/* deleteOnFailure= */ true,
|
||||||
|
addNewDownloadsAsCompleted);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
|
||||||
}
|
}
|
||||||
return downloadCache;
|
}
|
||||||
|
|
||||||
|
private DatabaseProvider getDatabaseProvider() {
|
||||||
|
if (databaseProvider == null) {
|
||||||
|
databaseProvider = new ExoDatabaseProvider(this);
|
||||||
|
}
|
||||||
|
return databaseProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getDownloadDirectory() {
|
private File getDownloadDirectory() {
|
||||||
|
|
@ -118,8 +160,8 @@ public class DemoApplication extends Application {
|
||||||
return downloadDirectory;
|
return downloadDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
|
||||||
DefaultDataSourceFactory upstreamFactory, Cache cache) {
|
DataSource.Factory upstreamFactory, Cache cache) {
|
||||||
return new CacheDataSourceFactory(
|
return new CacheDataSourceFactory(
|
||||||
cache,
|
cache,
|
||||||
upstreamFactory,
|
upstreamFactory,
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,14 @@
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
|
import com.google.android.exoplayer2.offline.Download;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager.TaskState;
|
|
||||||
import com.google.android.exoplayer2.offline.DownloadService;
|
import com.google.android.exoplayer2.offline.DownloadService;
|
||||||
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
import com.google.android.exoplayer2.scheduler.PlatformScheduler;
|
||||||
import com.google.android.exoplayer2.ui.DownloadNotificationUtil;
|
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
|
||||||
import com.google.android.exoplayer2.util.NotificationUtil;
|
import com.google.android.exoplayer2.util.NotificationUtil;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** A service for downloading media. */
|
/** A service for downloading media. */
|
||||||
public class DemoDownloadService extends DownloadService {
|
public class DemoDownloadService extends DownloadService {
|
||||||
|
|
@ -31,12 +32,24 @@ public class DemoDownloadService extends DownloadService {
|
||||||
private static final int JOB_ID = 1;
|
private static final int JOB_ID = 1;
|
||||||
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
private static final int FOREGROUND_NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
|
|
||||||
|
private DownloadNotificationHelper notificationHelper;
|
||||||
|
|
||||||
public DemoDownloadService() {
|
public DemoDownloadService() {
|
||||||
super(
|
super(
|
||||||
FOREGROUND_NOTIFICATION_ID,
|
FOREGROUND_NOTIFICATION_ID,
|
||||||
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
R.string.exo_download_notification_channel_name);
|
R.string.exo_download_notification_channel_name,
|
||||||
|
/* channelDescriptionResourceId= */ 0);
|
||||||
|
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -50,40 +63,29 @@ public class DemoDownloadService extends DownloadService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Notification getForegroundNotification(TaskState[] taskStates) {
|
protected Notification getForegroundNotification(List<Download> downloads) {
|
||||||
return DownloadNotificationUtil.buildProgressNotification(
|
return notificationHelper.buildProgressNotification(
|
||||||
/* context= */ this,
|
R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
|
||||||
R.drawable.exo_controls_play,
|
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
|
||||||
/* message= */ null,
|
|
||||||
taskStates);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onTaskStateChanged(TaskState taskState) {
|
protected void onDownloadChanged(Download download) {
|
||||||
if (taskState.action.isRemoveAction) {
|
Notification notification;
|
||||||
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
|
notification =
|
||||||
|
notificationHelper.buildDownloadCompletedNotification(
|
||||||
|
R.drawable.ic_download_done,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(download.request.data));
|
||||||
|
} else if (download.state == Download.STATE_FAILED) {
|
||||||
|
notification =
|
||||||
|
notificationHelper.buildDownloadFailedNotification(
|
||||||
|
R.drawable.ic_download_done,
|
||||||
|
/* contentIntent= */ null,
|
||||||
|
Util.fromUtf8Bytes(download.request.data));
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Notification notification = null;
|
NotificationUtil.setNotification(this, nextNotificationId++, notification);
|
||||||
if (taskState.state == TaskState.STATE_COMPLETED) {
|
|
||||||
notification =
|
|
||||||
DownloadNotificationUtil.buildDownloadCompletedNotification(
|
|
||||||
/* context= */ this,
|
|
||||||
R.drawable.exo_controls_play,
|
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
|
||||||
Util.fromUtf8Bytes(taskState.action.data));
|
|
||||||
} else if (taskState.state == TaskState.STATE_FAILED) {
|
|
||||||
notification =
|
|
||||||
DownloadNotificationUtil.buildDownloadFailedNotification(
|
|
||||||
/* context= */ this,
|
|
||||||
R.drawable.exo_controls_play,
|
|
||||||
CHANNEL_ID,
|
|
||||||
/* contentIntent= */ null,
|
|
||||||
Util.fromUtf8Bytes(taskState.action.data));
|
|
||||||
}
|
|
||||||
int notificationId = FOREGROUND_NOTIFICATION_ID + 1 + taskState.taskId;
|
|
||||||
NotificationUtil.setNotification(this, notificationId, notification);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,54 +15,32 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import androidx.annotation.Nullable;
|
||||||
import android.os.HandlerThread;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.offline.ActionFile;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.offline.DownloadAction;
|
import com.google.android.exoplayer2.offline.Download;
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadCursor;
|
||||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||||
|
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.DownloadManager.TaskState;
|
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.offline.ProgressiveDownloadHelper;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
import com.google.android.exoplayer2.offline.TrackKey;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
|
||||||
import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper;
|
|
||||||
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
|
|
||||||
import com.google.android.exoplayer2.ui.TrackNameProvider;
|
|
||||||
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.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
/**
|
/** Tracks media that has been downloaded. */
|
||||||
* Tracks media that has been downloaded.
|
public class DownloadTracker {
|
||||||
*
|
|
||||||
* <p>Tracked downloads are persisted using an {@link ActionFile}, however in a real application
|
|
||||||
* it's expected that state will be stored directly in the application's media database, so that it
|
|
||||||
* can be queried efficiently together with other information about the media.
|
|
||||||
*/
|
|
||||||
public class DownloadTracker implements DownloadManager.Listener {
|
|
||||||
|
|
||||||
/** Listens for changes in the tracked downloads. */
|
/** Listens for changes in the tracked downloads. */
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
|
|
@ -75,28 +53,23 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final DataSource.Factory dataSourceFactory;
|
private final DataSource.Factory dataSourceFactory;
|
||||||
private final TrackNameProvider trackNameProvider;
|
|
||||||
private final CopyOnWriteArraySet<Listener> listeners;
|
private final CopyOnWriteArraySet<Listener> listeners;
|
||||||
private final HashMap<Uri, DownloadAction> trackedDownloadStates;
|
private final HashMap<Uri, Download> downloads;
|
||||||
private final ActionFile actionFile;
|
private final DownloadIndex downloadIndex;
|
||||||
private final Handler actionFileWriteHandler;
|
private final DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||||
|
|
||||||
|
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
|
||||||
|
|
||||||
public DownloadTracker(
|
public DownloadTracker(
|
||||||
Context context,
|
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
|
||||||
DataSource.Factory dataSourceFactory,
|
|
||||||
File actionFile,
|
|
||||||
DownloadAction.Deserializer... deserializers) {
|
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
this.actionFile = new ActionFile(actionFile);
|
|
||||||
trackNameProvider = new DefaultTrackNameProvider(context.getResources());
|
|
||||||
listeners = new CopyOnWriteArraySet<>();
|
listeners = new CopyOnWriteArraySet<>();
|
||||||
trackedDownloadStates = new HashMap<>();
|
downloads = new HashMap<>();
|
||||||
HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker");
|
downloadIndex = downloadManager.getDownloadIndex();
|
||||||
actionFileWriteThread.start();
|
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
|
||||||
actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper());
|
downloadManager.addListener(new DownloadManagerListener());
|
||||||
loadTrackedActions(
|
loadDownloads();
|
||||||
deserializers.length > 0 ? deserializers : DownloadAction.getDefaultDeserializers());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(Listener listener) {
|
public void addListener(Listener listener) {
|
||||||
|
|
@ -108,191 +81,189 @@ public class DownloadTracker implements DownloadManager.Listener {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDownloaded(Uri uri) {
|
public boolean isDownloaded(Uri uri) {
|
||||||
return trackedDownloadStates.containsKey(uri);
|
Download download = downloads.get(uri);
|
||||||
|
return download != null && download.state != Download.STATE_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||||
public List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
Download download = downloads.get(uri);
|
||||||
if (!trackedDownloadStates.containsKey(uri)) {
|
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
return trackedDownloadStates.get(uri).getKeys();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleDownload(Activity activity, String name, Uri uri, String extension) {
|
public void toggleDownload(
|
||||||
if (isDownloaded(uri)) {
|
FragmentManager fragmentManager,
|
||||||
DownloadAction removeAction =
|
String name,
|
||||||
getDownloadHelper(uri, extension).getRemoveAction(Util.getUtf8Bytes(name));
|
Uri uri,
|
||||||
startServiceWithAction(removeAction);
|
String extension,
|
||||||
|
RenderersFactory renderersFactory) {
|
||||||
|
Download download = downloads.get(uri);
|
||||||
|
if (download != null) {
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
||||||
} else {
|
} else {
|
||||||
StartDownloadDialogHelper helper =
|
if (startDownloadDialogHelper != null) {
|
||||||
new StartDownloadDialogHelper(activity, getDownloadHelper(uri, extension), name);
|
startDownloadDialogHelper.release();
|
||||||
helper.prepare();
|
}
|
||||||
|
startDownloadDialogHelper =
|
||||||
|
new StartDownloadDialogHelper(
|
||||||
|
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadManager.Listener
|
private void loadDownloads() {
|
||||||
|
try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
|
||||||
@Override
|
while (loadedDownloads.moveToNext()) {
|
||||||
public void onInitialized(DownloadManager downloadManager) {
|
Download download = loadedDownloads.getDownload();
|
||||||
// Do nothing.
|
downloads.put(download.request.uri, download);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskStateChanged(DownloadManager downloadManager, TaskState taskState) {
|
|
||||||
DownloadAction action = taskState.action;
|
|
||||||
Uri uri = action.uri;
|
|
||||||
if ((action.isRemoveAction && taskState.state == TaskState.STATE_COMPLETED)
|
|
||||||
|| (!action.isRemoveAction && taskState.state == TaskState.STATE_FAILED)) {
|
|
||||||
// A download has been removed, or has failed. Stop tracking it.
|
|
||||||
if (trackedDownloadStates.remove(uri) != null) {
|
|
||||||
handleTrackedDownloadStatesChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onIdle(DownloadManager downloadManager) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal methods
|
|
||||||
|
|
||||||
private void loadTrackedActions(DownloadAction.Deserializer[] deserializers) {
|
|
||||||
try {
|
|
||||||
DownloadAction[] allActions = actionFile.load(deserializers);
|
|
||||||
for (DownloadAction action : allActions) {
|
|
||||||
trackedDownloadStates.put(action.uri, action);
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Failed to load tracked actions", e);
|
Log.w(TAG, "Failed to query downloads", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTrackedDownloadStatesChanged() {
|
private DownloadHelper getDownloadHelper(
|
||||||
for (Listener listener : listeners) {
|
Uri uri, String extension, RenderersFactory renderersFactory) {
|
||||||
listener.onDownloadsChanged();
|
|
||||||
}
|
|
||||||
final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]);
|
|
||||||
actionFileWriteHandler.post(
|
|
||||||
() -> {
|
|
||||||
try {
|
|
||||||
actionFile.store(actions);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to store tracked actions", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startDownload(DownloadAction action) {
|
|
||||||
if (trackedDownloadStates.containsKey(action.uri)) {
|
|
||||||
// This content is already being downloaded. Do nothing.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trackedDownloadStates.put(action.uri, action);
|
|
||||||
handleTrackedDownloadStatesChanged();
|
|
||||||
startServiceWithAction(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startServiceWithAction(DownloadAction action) {
|
|
||||||
DownloadService.startWithAction(context, DemoDownloadService.class, action, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DownloadHelper getDownloadHelper(Uri uri, String extension) {
|
|
||||||
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 new DashDownloadHelper(uri, dataSourceFactory);
|
return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsDownloadHelper(uri, dataSourceFactory);
|
return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsDownloadHelper(uri, dataSourceFactory);
|
return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ProgressiveDownloadHelper(uri);
|
return DownloadHelper.forProgressive(context, uri);
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class StartDownloadDialogHelper
|
private class DownloadManagerListener implements DownloadManager.Listener {
|
||||||
implements DownloadHelper.Callback, DialogInterface.OnClickListener {
|
|
||||||
|
|
||||||
private final DownloadHelper downloadHelper;
|
@Override
|
||||||
private final String name;
|
public void onDownloadChanged(DownloadManager downloadManager, Download download) {
|
||||||
|
downloads.put(download.request.uri, download);
|
||||||
private final AlertDialog.Builder builder;
|
for (Listener listener : listeners) {
|
||||||
private final View dialogView;
|
listener.onDownloadsChanged();
|
||||||
private final List<TrackKey> trackKeys;
|
|
||||||
private final ArrayAdapter<String> trackTitles;
|
|
||||||
private final ListView representationList;
|
|
||||||
|
|
||||||
public StartDownloadDialogHelper(
|
|
||||||
Activity activity, DownloadHelper downloadHelper, String name) {
|
|
||||||
this.downloadHelper = downloadHelper;
|
|
||||||
this.name = name;
|
|
||||||
builder =
|
|
||||||
new AlertDialog.Builder(activity)
|
|
||||||
.setTitle(R.string.exo_download_description)
|
|
||||||
.setPositiveButton(android.R.string.ok, this)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null);
|
|
||||||
|
|
||||||
// Inflate with the builder's context to ensure the correct style is used.
|
|
||||||
LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext());
|
|
||||||
dialogView = dialogInflater.inflate(R.layout.start_download_dialog, null);
|
|
||||||
|
|
||||||
trackKeys = new ArrayList<>();
|
|
||||||
trackTitles =
|
|
||||||
new ArrayAdapter<>(
|
|
||||||
builder.getContext(), android.R.layout.simple_list_item_multiple_choice);
|
|
||||||
representationList = dialogView.findViewById(R.id.representation_list);
|
|
||||||
representationList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
|
|
||||||
representationList.setAdapter(trackTitles);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepare() {
|
|
||||||
downloadHelper.prepare(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
|
||||||
|
downloads.remove(download.request.uri);
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onDownloadsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class StartDownloadDialogHelper
|
||||||
|
implements DownloadHelper.Callback,
|
||||||
|
DialogInterface.OnClickListener,
|
||||||
|
DialogInterface.OnDismissListener {
|
||||||
|
|
||||||
|
private final FragmentManager fragmentManager;
|
||||||
|
private final DownloadHelper downloadHelper;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private TrackSelectionDialog trackSelectionDialog;
|
||||||
|
private MappedTrackInfo mappedTrackInfo;
|
||||||
|
|
||||||
|
public StartDownloadDialogHelper(
|
||||||
|
FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
|
||||||
|
this.fragmentManager = fragmentManager;
|
||||||
|
this.downloadHelper = downloadHelper;
|
||||||
|
this.name = name;
|
||||||
|
downloadHelper.prepare(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
downloadHelper.release();
|
||||||
|
if (trackSelectionDialog != null) {
|
||||||
|
trackSelectionDialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadHelper.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepared(DownloadHelper helper) {
|
public void onPrepared(DownloadHelper helper) {
|
||||||
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
|
if (helper.getPeriodCount() == 0) {
|
||||||
TrackGroupArray trackGroups = downloadHelper.getTrackGroups(i);
|
Log.d(TAG, "No periods found. Downloading entire stream.");
|
||||||
for (int j = 0; j < trackGroups.length; j++) {
|
startDownload();
|
||||||
TrackGroup trackGroup = trackGroups.get(j);
|
downloadHelper.release();
|
||||||
for (int k = 0; k < trackGroup.length; k++) {
|
return;
|
||||||
trackKeys.add(new TrackKey(i, j, k));
|
|
||||||
trackTitles.add(trackNameProvider.getTrackName(trackGroup.getFormat(k)));
|
|
||||||
}
|
}
|
||||||
|
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
|
||||||
|
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
|
||||||
|
Log.d(TAG, "No dialog content. Downloading entire stream.");
|
||||||
|
startDownload();
|
||||||
|
downloadHelper.release();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
trackSelectionDialog =
|
||||||
if (!trackKeys.isEmpty()) {
|
TrackSelectionDialog.createForMappedTrackInfoAndParameters(
|
||||||
builder.setView(dialogView);
|
/* titleId= */ R.string.exo_download_description,
|
||||||
}
|
mappedTrackInfo,
|
||||||
builder.create().show();
|
trackSelectorParameters,
|
||||||
|
/* allowAdaptiveSelections =*/ false,
|
||||||
|
/* allowMultipleOverrides= */ true,
|
||||||
|
/* onClickListener= */ this,
|
||||||
|
/* onDismissListener= */ this);
|
||||||
|
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialogInterface.OnClickListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
ArrayList<TrackKey> selectedTrackKeys = new ArrayList<>();
|
for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
|
||||||
for (int i = 0; i < representationList.getChildCount(); i++) {
|
downloadHelper.clearTrackSelections(periodIndex);
|
||||||
if (representationList.isItemChecked(i)) {
|
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
||||||
selectedTrackKeys.add(trackKeys.get(i));
|
if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
|
||||||
}
|
downloadHelper.addTrackSelectionForSingleRenderer(
|
||||||
}
|
periodIndex,
|
||||||
if (!selectedTrackKeys.isEmpty() || trackKeys.isEmpty()) {
|
/* rendererIndex= */ i,
|
||||||
// We have selected keys, or we're dealing with single stream content.
|
trackSelectorParameters,
|
||||||
DownloadAction downloadAction =
|
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
|
||||||
downloadHelper.getDownloadAction(Util.getUtf8Bytes(name), selectedTrackKeys);
|
|
||||||
startDownload(downloadAction);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DownloadRequest downloadRequest = buildDownloadRequest();
|
||||||
|
if (downloadRequest.streamKeys.isEmpty()) {
|
||||||
|
// All tracks were deselected in the dialog. Don't start the download.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startDownload(downloadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialogInterface.OnDismissListener implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialogInterface) {
|
||||||
|
trackSelectionDialog = null;
|
||||||
|
downloadHelper.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void startDownload() {
|
||||||
|
startDownload(buildDownloadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDownload(DownloadRequest downloadRequest) {
|
||||||
|
DownloadService.sendAddDownload(
|
||||||
|
context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadRequest buildDownloadRequest() {
|
||||||
|
return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,54 +15,51 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.C.ContentType;
|
import com.google.android.exoplayer2.C.ContentType;
|
||||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
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.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;
|
||||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||||
import com.google.android.exoplayer2.offline.FilteringManifestParser;
|
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory;
|
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
|
|
@ -72,7 +69,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
|
||||||
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.exoplayer2.ui.TrackSelectionView;
|
|
||||||
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
|
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
|
|
@ -83,42 +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.List;
|
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 Activity
|
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";
|
||||||
|
|
@ -130,13 +132,16 @@ public class PlayerActivity extends Activity
|
||||||
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 TextView debugTextView;
|
private TextView debugTextView;
|
||||||
|
private boolean isShowingTrackSelectionDialog;
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -151,13 +156,17 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
private AdsLoader adsLoader;
|
private AdsLoader adsLoader;
|
||||||
private Uri loadedAdTagUri;
|
private Uri loadedAdTagUri;
|
||||||
private ViewGroup adUiViewGroup;
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
@ -168,10 +177,10 @@ public class PlayerActivity extends Activity
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentView(R.layout.player_activity);
|
setContentView(R.layout.player_activity);
|
||||||
View rootView = findViewById(R.id.root);
|
|
||||||
rootView.setOnClickListener(this);
|
|
||||||
debugRootView = findViewById(R.id.controls_root);
|
debugRootView = findViewById(R.id.controls_root);
|
||||||
debugTextView = findViewById(R.id.debug_text_view);
|
debugTextView = findViewById(R.id.debug_text_view);
|
||||||
|
selectTracksButton = findViewById(R.id.select_tracks_button);
|
||||||
|
selectTracksButton.setOnClickListener(this);
|
||||||
|
|
||||||
playerView = findViewById(R.id.player_view);
|
playerView = findViewById(R.id.player_view);
|
||||||
playerView.setControllerVisibilityListener(this);
|
playerView.setControllerVisibilityListener(this);
|
||||||
|
|
@ -199,13 +208,14 @@ public class PlayerActivity extends Activity
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewIntent(Intent intent) {
|
public void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
releasePlayer();
|
releasePlayer();
|
||||||
releaseAdsLoader();
|
releaseAdsLoader();
|
||||||
clearStartPosition();
|
clearStartPosition();
|
||||||
|
|
@ -280,6 +290,7 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
updateTrackSelectorParameters();
|
updateTrackSelectorParameters();
|
||||||
updateStartPosition();
|
updateStartPosition();
|
||||||
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
|
outState.putParcelable(KEY_TRACK_SELECTOR_PARAMETERS, trackSelectorParameters);
|
||||||
|
|
@ -300,23 +311,15 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (view.getParent() == debugRootView) {
|
if (view == selectTracksButton
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
&& !isShowingTrackSelectionDialog
|
||||||
if (mappedTrackInfo != null) {
|
&& TrackSelectionDialog.willHaveContent(trackSelector)) {
|
||||||
CharSequence title = ((Button) view).getText();
|
isShowingTrackSelectionDialog = true;
|
||||||
int rendererIndex = (int) view.getTag();
|
TrackSelectionDialog trackSelectionDialog =
|
||||||
int rendererType = mappedTrackInfo.getRendererType(rendererIndex);
|
TrackSelectionDialog.createForTrackSelector(
|
||||||
boolean allowAdaptiveSelections =
|
trackSelector,
|
||||||
rendererType == C.TRACK_TYPE_VIDEO
|
/* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);
|
||||||
|| (rendererType == C.TRACK_TYPE_AUDIO
|
trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);
|
||||||
&& mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
|
||||||
== MappedTrackInfo.RENDERER_SUPPORT_NO_TRACKS);
|
|
||||||
Pair<AlertDialog, TrackSelectionView> dialogPair =
|
|
||||||
TrackSelectionView.getDialog(this, title, trackSelector, rendererIndex);
|
|
||||||
dialogPair.second.setShowDisableOption(true);
|
|
||||||
dialogPair.second.setAllowAdaptiveSelections(allowAdaptiveSelections);
|
|
||||||
dialogPair.first.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,7 +327,7 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preparePlayback() {
|
public void preparePlayback() {
|
||||||
initializePlayer();
|
player.retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaybackControlView.VisibilityListener implementation
|
// PlaybackControlView.VisibilityListener implementation
|
||||||
|
|
@ -339,68 +342,12 @@ public class PlayerActivity extends Activity
|
||||||
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 =
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
TrackSelection.Factory trackSelectionFactory;
|
TrackSelection.Factory trackSelectionFactory;
|
||||||
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
||||||
|
|
@ -416,21 +363,15 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
boolean preferExtensionDecoders =
|
boolean preferExtensionDecoders =
|
||||||
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
|
||||||
@DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode =
|
RenderersFactory renderersFactory =
|
||||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
|
||||||
? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
|
||||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
|
||||||
DefaultRenderersFactory renderersFactory =
|
|
||||||
new DefaultRenderersFactory(this, extensionRendererMode);
|
|
||||||
|
|
||||||
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));
|
||||||
|
|
@ -438,28 +379,8 @@ public class PlayerActivity extends Activity
|
||||||
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;
|
||||||
|
|
@ -467,43 +388,137 @@ public class PlayerActivity extends Activity
|
||||||
player.seekTo(startWindow, startPosition);
|
player.seekTo(startWindow, startPosition);
|
||||||
}
|
}
|
||||||
player.prepare(mediaSource, !haveStartPosition, false);
|
player.prepare(mediaSource, !haveStartPosition, false);
|
||||||
updateButtonVisibilities();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
Sample intentAsSample = Sample.createFromIntent(intent);
|
||||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
UriSample[] samples =
|
||||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
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 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 =
|
||||||
|
((DemoApplication) getApplication())
|
||||||
|
.getDownloadTracker()
|
||||||
|
.getDownloadRequest(parameters.uri);
|
||||||
|
if (downloadRequest != null) {
|
||||||
|
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
||||||
|
}
|
||||||
|
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)
|
return new DashMediaSource.Factory(dataSourceFactory)
|
||||||
.setManifestParser(
|
.setDrmSessionManager(drmSessionManager)
|
||||||
new FilteringManifestParser<>(new DashManifestParser(), getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_SS:
|
case C.TYPE_SS:
|
||||||
return new SsMediaSource.Factory(dataSourceFactory)
|
return new SsMediaSource.Factory(dataSourceFactory)
|
||||||
.setManifestParser(
|
.setDrmSessionManager(drmSessionManager)
|
||||||
new FilteringManifestParser<>(new SsManifestParser(), getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_HLS:
|
case C.TYPE_HLS:
|
||||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||||
.setPlaylistParserFactory(
|
.setDrmSessionManager(drmSessionManager)
|
||||||
new DefaultHlsPlaylistParserFactory(getOfflineStreamKeys(uri)))
|
|
||||||
.createMediaSource(uri);
|
.createMediaSource(uri);
|
||||||
case C.TYPE_OTHER:
|
case C.TYPE_OTHER:
|
||||||
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
return new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||||
default: {
|
.setDrmSessionManager(drmSessionManager)
|
||||||
|
.createMediaSource(uri);
|
||||||
|
default:
|
||||||
throw new IllegalStateException("Unsupported type: " + type);
|
throw new IllegalStateException("Unsupported type: " + type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
|
||||||
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
|
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
|
||||||
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||||
|
|
@ -518,8 +533,9 @@ public class PlayerActivity extends Activity
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -534,14 +550,17 @@ public class PlayerActivity extends Activity
|
||||||
mediaSource = null;
|
mediaSource = null;
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
}
|
}
|
||||||
releaseMediaDrm();
|
if (adsLoader != null) {
|
||||||
|
adsLoader.setPlayer(null);
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
|
@ -579,7 +598,8 @@ public class PlayerActivity extends Activity
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 {
|
||||||
|
|
@ -593,15 +613,13 @@ public class PlayerActivity extends Activity
|
||||||
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
.getConstructor(android.content.Context.class, android.net.Uri.class);
|
||||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||||
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
adsLoader = loaderConstructor.newInstance(this, adTagUri);
|
||||||
adUiViewGroup = new FrameLayout(this);
|
|
||||||
// The demo app has a non-null overlay frame layout.
|
|
||||||
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
|
|
||||||
}
|
}
|
||||||
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
|
MediaSourceFactory adMediaSourceFactory =
|
||||||
new AdsMediaSource.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
|
||||||
|
|
@ -609,7 +627,7 @@ public class PlayerActivity extends Activity
|
||||||
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup);
|
return new AdsMediaSource(mediaSource, adMediaSourceFactory, adsLoader, playerView);
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
// IMA extension not loaded.
|
// IMA extension not loaded.
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -620,41 +638,9 @@ public class PlayerActivity extends Activity
|
||||||
|
|
||||||
// User controls
|
// User controls
|
||||||
|
|
||||||
private void updateButtonVisibilities() {
|
private void updateButtonVisibility() {
|
||||||
debugRootView.removeAllViews();
|
selectTracksButton.setEnabled(
|
||||||
if (player == null) {
|
player != null && TrackSelectionDialog.willHaveContent(trackSelector));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
|
||||||
if (mappedTrackInfo == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
|
||||||
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
|
|
||||||
if (trackGroups.length != 0) {
|
|
||||||
Button button = new Button(this);
|
|
||||||
int label;
|
|
||||||
switch (player.getRendererType(i)) {
|
|
||||||
case C.TRACK_TYPE_AUDIO:
|
|
||||||
label = R.string.exo_track_selection_title_audio;
|
|
||||||
break;
|
|
||||||
case C.TRACK_TYPE_VIDEO:
|
|
||||||
label = R.string.exo_track_selection_title_video;
|
|
||||||
break;
|
|
||||||
case C.TRACK_TYPE_TEXT:
|
|
||||||
label = R.string.exo_track_selection_title_text;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
button.setText(label);
|
|
||||||
button.setTag(i);
|
|
||||||
button.setOnClickListener(this);
|
|
||||||
debugRootView.addView(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showControls() {
|
private void showControls() {
|
||||||
|
|
@ -686,20 +672,11 @@ public class PlayerActivity extends Activity
|
||||||
private class PlayerEventListener implements Player.EventListener {
|
private class PlayerEventListener implements Player.EventListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||||
if (playbackState == Player.STATE_ENDED) {
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
showControls();
|
showControls();
|
||||||
}
|
}
|
||||||
updateButtonVisibilities();
|
updateButtonVisibility();
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
@Override
|
||||||
|
|
@ -708,8 +685,7 @@ public class PlayerActivity extends Activity
|
||||||
clearStartPosition();
|
clearStartPosition();
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
} else {
|
} else {
|
||||||
updateStartPosition();
|
updateButtonVisibility();
|
||||||
updateButtonVisibilities();
|
|
||||||
showControls();
|
showControls();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -717,7 +693,7 @@ public class PlayerActivity extends Activity
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("ReferenceEquality")
|
@SuppressWarnings("ReferenceEquality")
|
||||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||||
updateButtonVisibilities();
|
updateButtonVisibility();
|
||||||
if (trackGroups != lastSeenTrackGroupArray) {
|
if (trackGroups != lastSeenTrackGroupArray) {
|
||||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||||
if (mappedTrackInfo != null) {
|
if (mappedTrackInfo != null) {
|
||||||
|
|
@ -746,7 +722,7 @@ public class PlayerActivity extends Activity
|
||||||
// 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) {
|
||||||
|
|
@ -761,12 +737,11 @@ public class PlayerActivity extends Activity
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
@ -15,16 +15,15 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.demo;
|
package com.google.android.exoplayer2.demo;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import android.util.JsonReader;
|
import android.util.JsonReader;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
@ -38,12 +37,17 @@ import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
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.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;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
@ -54,7 +58,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** An activity for selecting from a list of media samples. */
|
/** An activity for selecting from a list of media samples. */
|
||||||
public class SampleChooserActivity extends Activity
|
public class SampleChooserActivity extends AppCompatActivity
|
||||||
implements DownloadTracker.Listener, OnChildClickListener {
|
implements DownloadTracker.Listener, OnChildClickListener {
|
||||||
|
|
||||||
private static final String TAG = "SampleChooserActivity";
|
private static final String TAG = "SampleChooserActivity";
|
||||||
|
|
@ -160,13 +164,17 @@ public class SampleChooserActivity extends Activity
|
||||||
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));
|
||||||
|
String abrAlgorithm =
|
||||||
isNonNullAndChecked(randomAbrMenuItem)
|
isNonNullAndChecked(randomAbrMenuItem)
|
||||||
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
? PlayerActivity.ABR_ALGORITHM_RANDOM
|
||||||
: PlayerActivity.ABR_ALGORITHM_DEFAULT));
|
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
|
||||||
|
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
|
||||||
|
sample.addToIntent(intent);
|
||||||
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +185,15 @@ public class SampleChooserActivity extends Activity
|
||||||
.show();
|
.show();
|
||||||
} else {
|
} else {
|
||||||
UriSample uriSample = (UriSample) sample;
|
UriSample uriSample = (UriSample) sample;
|
||||||
downloadTracker.toggleDownload(this, sample.name, uriSample.uri, uriSample.extension);
|
RenderersFactory renderersFactory =
|
||||||
|
((DemoApplication) getApplication())
|
||||||
|
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
|
||||||
|
downloadTracker.toggleDownload(
|
||||||
|
getSupportFragmentManager(),
|
||||||
|
sample.name,
|
||||||
|
uriSample.uri,
|
||||||
|
uriSample.extension,
|
||||||
|
renderersFactory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,17 +316,12 @@ public class SampleChooserActivity extends Activity
|
||||||
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()) {
|
||||||
|
|
@ -348,18 +359,21 @@ public class SampleChooserActivity extends Activity
|
||||||
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(
|
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
|
||||||
new UriSample[playlistSamples.size()]);
|
return new PlaylistSample(sampleName, playlistSamplesArray);
|
||||||
return new PlaylistSample(sampleName, drmInfo, 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -489,116 +503,4 @@ public class SampleChooserActivity extends Activity
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,368 @@
|
||||||
|
/*
|
||||||
|
* 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 android.app.Dialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
|
import androidx.appcompat.app.AppCompatDialog;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
|
||||||
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||||
|
import com.google.android.exoplayer2.ui.TrackSelectionView;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Dialog to select tracks. */
|
||||||
|
public final class TrackSelectionDialog extends DialogFragment {
|
||||||
|
|
||||||
|
private final SparseArray<TrackSelectionViewFragment> tabFragments;
|
||||||
|
private final ArrayList<Integer> tabTrackTypes;
|
||||||
|
|
||||||
|
private int titleId;
|
||||||
|
private DialogInterface.OnClickListener onClickListener;
|
||||||
|
private DialogInterface.OnDismissListener onDismissListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a track selection dialog will have content to display if initialized with the
|
||||||
|
* specified {@link DefaultTrackSelector} in its current state.
|
||||||
|
*/
|
||||||
|
public static boolean willHaveContent(DefaultTrackSelector trackSelector) {
|
||||||
|
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||||
|
return mappedTrackInfo != null && willHaveContent(mappedTrackInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a track selection dialog will have content to display if initialized with the
|
||||||
|
* specified {@link MappedTrackInfo}.
|
||||||
|
*/
|
||||||
|
public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) {
|
||||||
|
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
||||||
|
if (showTabForRenderer(mappedTrackInfo, i)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be
|
||||||
|
* automatically updated when tracks are selected.
|
||||||
|
*
|
||||||
|
* @param trackSelector The {@link DefaultTrackSelector}.
|
||||||
|
* @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is
|
||||||
|
* dismissed.
|
||||||
|
*/
|
||||||
|
public static TrackSelectionDialog createForTrackSelector(
|
||||||
|
DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) {
|
||||||
|
MappedTrackInfo mappedTrackInfo =
|
||||||
|
Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo());
|
||||||
|
TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
|
||||||
|
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters();
|
||||||
|
trackSelectionDialog.init(
|
||||||
|
/* titleId= */ R.string.track_selection_title,
|
||||||
|
mappedTrackInfo,
|
||||||
|
/* initialParameters = */ parameters,
|
||||||
|
/* allowAdaptiveSelections =*/ true,
|
||||||
|
/* allowMultipleOverrides= */ false,
|
||||||
|
/* onClickListener= */ (dialog, which) -> {
|
||||||
|
DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
|
||||||
|
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
||||||
|
builder
|
||||||
|
.clearSelectionOverrides(/* rendererIndex= */ i)
|
||||||
|
.setRendererDisabled(
|
||||||
|
/* rendererIndex= */ i,
|
||||||
|
trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i));
|
||||||
|
List<SelectionOverride> overrides =
|
||||||
|
trackSelectionDialog.getOverrides(/* rendererIndex= */ i);
|
||||||
|
if (!overrides.isEmpty()) {
|
||||||
|
builder.setSelectionOverride(
|
||||||
|
/* rendererIndex= */ i,
|
||||||
|
mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i),
|
||||||
|
overrides.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackSelector.setParameters(builder);
|
||||||
|
},
|
||||||
|
onDismissListener);
|
||||||
|
return trackSelectionDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}.
|
||||||
|
*
|
||||||
|
* @param titleId The resource id of the dialog title.
|
||||||
|
* @param mappedTrackInfo The {@link MappedTrackInfo} to display.
|
||||||
|
* @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial
|
||||||
|
* track selection.
|
||||||
|
* @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track)
|
||||||
|
* can be made.
|
||||||
|
* @param allowMultipleOverrides Whether tracks from multiple track groups can be selected.
|
||||||
|
* @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected.
|
||||||
|
* @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is
|
||||||
|
* dismissed.
|
||||||
|
*/
|
||||||
|
public static TrackSelectionDialog createForMappedTrackInfoAndParameters(
|
||||||
|
int titleId,
|
||||||
|
MappedTrackInfo mappedTrackInfo,
|
||||||
|
DefaultTrackSelector.Parameters initialParameters,
|
||||||
|
boolean allowAdaptiveSelections,
|
||||||
|
boolean allowMultipleOverrides,
|
||||||
|
DialogInterface.OnClickListener onClickListener,
|
||||||
|
DialogInterface.OnDismissListener onDismissListener) {
|
||||||
|
TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
|
||||||
|
trackSelectionDialog.init(
|
||||||
|
titleId,
|
||||||
|
mappedTrackInfo,
|
||||||
|
initialParameters,
|
||||||
|
allowAdaptiveSelections,
|
||||||
|
allowMultipleOverrides,
|
||||||
|
onClickListener,
|
||||||
|
onDismissListener);
|
||||||
|
return trackSelectionDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackSelectionDialog() {
|
||||||
|
tabFragments = new SparseArray<>();
|
||||||
|
tabTrackTypes = new ArrayList<>();
|
||||||
|
// Retain instance across activity re-creation to prevent losing access to init data.
|
||||||
|
setRetainInstance(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(
|
||||||
|
int titleId,
|
||||||
|
MappedTrackInfo mappedTrackInfo,
|
||||||
|
DefaultTrackSelector.Parameters initialParameters,
|
||||||
|
boolean allowAdaptiveSelections,
|
||||||
|
boolean allowMultipleOverrides,
|
||||||
|
DialogInterface.OnClickListener onClickListener,
|
||||||
|
DialogInterface.OnDismissListener onDismissListener) {
|
||||||
|
this.titleId = titleId;
|
||||||
|
this.onClickListener = onClickListener;
|
||||||
|
this.onDismissListener = onDismissListener;
|
||||||
|
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
|
||||||
|
if (showTabForRenderer(mappedTrackInfo, i)) {
|
||||||
|
int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i);
|
||||||
|
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);
|
||||||
|
TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment();
|
||||||
|
tabFragment.init(
|
||||||
|
mappedTrackInfo,
|
||||||
|
/* rendererIndex= */ i,
|
||||||
|
initialParameters.getRendererDisabled(/* rendererIndex= */ i),
|
||||||
|
initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray),
|
||||||
|
allowAdaptiveSelections,
|
||||||
|
allowMultipleOverrides);
|
||||||
|
tabFragments.put(i, tabFragment);
|
||||||
|
tabTrackTypes.add(trackType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a renderer is disabled.
|
||||||
|
*
|
||||||
|
* @param rendererIndex Renderer index.
|
||||||
|
* @return Whether the renderer is disabled.
|
||||||
|
*/
|
||||||
|
public boolean getIsDisabled(int rendererIndex) {
|
||||||
|
TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
|
||||||
|
return rendererView != null && rendererView.isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of selected track selection overrides for the specified renderer. There will
|
||||||
|
* be at most one override for each track group.
|
||||||
|
*
|
||||||
|
* @param rendererIndex Renderer index.
|
||||||
|
* @return The list of track selection overrides for this renderer.
|
||||||
|
*/
|
||||||
|
public List<SelectionOverride> getOverrides(int rendererIndex) {
|
||||||
|
TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex);
|
||||||
|
return rendererView == null ? Collections.emptyList() : rendererView.overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
// We need to own the view to let tab layout work correctly on all API levels. We can't use
|
||||||
|
// AlertDialog because it owns the view itself, so we use AppCompatDialog instead, themed using
|
||||||
|
// the AlertDialog theme overlay with force-enabled title.
|
||||||
|
AppCompatDialog dialog =
|
||||||
|
new AppCompatDialog(getActivity(), R.style.TrackSelectionDialogThemeOverlay);
|
||||||
|
dialog.setTitle(titleId);
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDismiss(DialogInterface dialog) {
|
||||||
|
super.onDismiss(dialog);
|
||||||
|
onDismissListener.onDismiss(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(
|
||||||
|
LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
|
||||||
|
View dialogView = inflater.inflate(R.layout.track_selection_dialog, container, false);
|
||||||
|
TabLayout tabLayout = dialogView.findViewById(R.id.track_selection_dialog_tab_layout);
|
||||||
|
ViewPager viewPager = dialogView.findViewById(R.id.track_selection_dialog_view_pager);
|
||||||
|
Button cancelButton = dialogView.findViewById(R.id.track_selection_dialog_cancel_button);
|
||||||
|
Button okButton = dialogView.findViewById(R.id.track_selection_dialog_ok_button);
|
||||||
|
viewPager.setAdapter(new FragmentAdapter(getChildFragmentManager()));
|
||||||
|
tabLayout.setupWithViewPager(viewPager);
|
||||||
|
tabLayout.setVisibility(tabFragments.size() > 1 ? View.VISIBLE : View.GONE);
|
||||||
|
cancelButton.setOnClickListener(view -> dismiss());
|
||||||
|
okButton.setOnClickListener(
|
||||||
|
view -> {
|
||||||
|
onClickListener.onClick(getDialog(), DialogInterface.BUTTON_POSITIVE);
|
||||||
|
dismiss();
|
||||||
|
});
|
||||||
|
return dialogView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) {
|
||||||
|
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
|
||||||
|
if (trackGroupArray.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int trackType = mappedTrackInfo.getRendererType(rendererIndex);
|
||||||
|
return isSupportedTrackType(trackType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSupportedTrackType(int trackType) {
|
||||||
|
switch (trackType) {
|
||||||
|
case C.TRACK_TYPE_VIDEO:
|
||||||
|
case C.TRACK_TYPE_AUDIO:
|
||||||
|
case C.TRACK_TYPE_TEXT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTrackTypeString(Resources resources, int trackType) {
|
||||||
|
switch (trackType) {
|
||||||
|
case C.TRACK_TYPE_VIDEO:
|
||||||
|
return resources.getString(R.string.exo_track_selection_title_video);
|
||||||
|
case C.TRACK_TYPE_AUDIO:
|
||||||
|
return resources.getString(R.string.exo_track_selection_title_audio);
|
||||||
|
case C.TRACK_TYPE_TEXT:
|
||||||
|
return resources.getString(R.string.exo_track_selection_title_text);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FragmentAdapter extends FragmentPagerAdapter {
|
||||||
|
|
||||||
|
public FragmentAdapter(FragmentManager fragmentManager) {
|
||||||
|
super(fragmentManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Fragment getItem(int position) {
|
||||||
|
return tabFragments.valueAt(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return tabFragments.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public CharSequence getPageTitle(int position) {
|
||||||
|
return getTrackTypeString(getResources(), tabTrackTypes.get(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fragment to show a track selection in tab of the track selection dialog. */
|
||||||
|
public static final class TrackSelectionViewFragment extends Fragment
|
||||||
|
implements TrackSelectionView.TrackSelectionListener {
|
||||||
|
|
||||||
|
private MappedTrackInfo mappedTrackInfo;
|
||||||
|
private int rendererIndex;
|
||||||
|
private boolean allowAdaptiveSelections;
|
||||||
|
private boolean allowMultipleOverrides;
|
||||||
|
|
||||||
|
/* package */ boolean isDisabled;
|
||||||
|
/* package */ List<SelectionOverride> overrides;
|
||||||
|
|
||||||
|
public TrackSelectionViewFragment() {
|
||||||
|
// Retain instance across activity re-creation to prevent losing access to init data.
|
||||||
|
setRetainInstance(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(
|
||||||
|
MappedTrackInfo mappedTrackInfo,
|
||||||
|
int rendererIndex,
|
||||||
|
boolean initialIsDisabled,
|
||||||
|
@Nullable SelectionOverride initialOverride,
|
||||||
|
boolean allowAdaptiveSelections,
|
||||||
|
boolean allowMultipleOverrides) {
|
||||||
|
this.mappedTrackInfo = mappedTrackInfo;
|
||||||
|
this.rendererIndex = rendererIndex;
|
||||||
|
this.isDisabled = initialIsDisabled;
|
||||||
|
this.overrides =
|
||||||
|
initialOverride == null
|
||||||
|
? Collections.emptyList()
|
||||||
|
: Collections.singletonList(initialOverride);
|
||||||
|
this.allowAdaptiveSelections = allowAdaptiveSelections;
|
||||||
|
this.allowMultipleOverrides = allowMultipleOverrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(
|
||||||
|
LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
View rootView =
|
||||||
|
inflater.inflate(
|
||||||
|
R.layout.exo_track_selection_dialog, container, /* attachToRoot= */ false);
|
||||||
|
TrackSelectionView trackSelectionView = rootView.findViewById(R.id.exo_track_selection_view);
|
||||||
|
trackSelectionView.setShowDisableOption(true);
|
||||||
|
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
|
||||||
|
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
|
||||||
|
trackSelectionView.init(
|
||||||
|
mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides) {
|
||||||
|
this.isDisabled = isDisabled;
|
||||||
|
this.overrides = overrides;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 303 B After Width: | Height: | Size: 261 B |
|
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 263 B |
|
|
@ -42,7 +42,15 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:visibility="gone"/>
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<Button android:id="@+id/select_tracks_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/track_selection_title"
|
||||||
|
android:enabled="false"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
|
||||||
59
demos/main/src/main/res/layout/track_selection_dialog.xml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.viewpager.widget.ViewPager
|
||||||
|
android:id="@+id/track_selection_dialog_view_pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/track_selection_dialog_tab_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:tabGravity="fill"
|
||||||
|
app:tabMode="fixed"/>
|
||||||
|
|
||||||
|
</androidx.viewpager.widget.ViewPager>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/track_selection_dialog_cancel_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@android:string/cancel"
|
||||||
|
style="?android:attr/borderlessButtonStyle"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/track_selection_dialog_ok_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@android:string/ok"
|
||||||
|
style="?android:attr/borderlessButtonStyle"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -13,13 +13,14 @@
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<item android:id="@+id/prefer_extension_decoders"
|
<item android:id="@+id/prefer_extension_decoders"
|
||||||
android:title="@string/prefer_extension_decoders"
|
android:title="@string/prefer_extension_decoders"
|
||||||
android:showAsAction="never"
|
android:checkable="true"
|
||||||
android:checkable="true"/>
|
app:showAsAction="never"/>
|
||||||
<item android:id="@+id/random_abr"
|
<item android:id="@+id/random_abr"
|
||||||
android:title="@string/random_abr"
|
android:title="@string/random_abr"
|
||||||
android:showAsAction="never"
|
android:checkable="true"
|
||||||
android:checkable="true"/>
|
app:showAsAction="never"/>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
<string name="application_name">ExoPlayer</string>
|
<string name="application_name">ExoPlayer</string>
|
||||||
|
|
||||||
|
<string name="track_selection_title">Select tracks</string>
|
||||||
|
|
||||||
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
|
||||||
|
|
||||||
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
||||||
|
|
@ -51,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>
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,11 @@
|
||||||
-->
|
-->
|
||||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<style name="PlayerTheme" parent="android:Theme.Holo">
|
<style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="windowNoTitle">false</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
|
||||||
<item name="android:windowBackground">@android:color/black</item>
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
The cast extension is a [Player][] implementation that controls playback on a
|
The cast extension is a [Player][] implementation that controls playback on a
|
||||||
Cast receiver app.
|
Cast receiver app.
|
||||||
|
|
||||||
[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html
|
[Player]: https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/Player.html
|
||||||
|
|
||||||
## Getting the extension ##
|
## Getting the extension ##
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
@ -24,30 +23,22 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.android.gms:play-services-cast-framework:16.0.1'
|
api 'com.google.android.gms:play-services-cast-framework:17.0.0'
|
||||||
|
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-compat-qual:' + checkerframeworkVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation 'junit:junit:' + junitVersion
|
|
||||||
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
|
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
|
||||||
// These dependencies are necessary to force the supportLibraryVersion of
|
|
||||||
// com.android.support:support-v4, com.android.support:appcompat-v7 and
|
|
||||||
// com.android.support:mediarouter-v7 to be used. Else older versions are
|
|
||||||
// used, for example via:
|
|
||||||
// com.google.android.gms:play-services-cast-framework:15.0.1
|
|
||||||
// |-- com.android.support:mediarouter-v7:26.1.0
|
|
||||||
api 'com.android.support:support-v4:' + supportLibraryVersion
|
|
||||||
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
|
|
||||||
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# Proguard rules specific to the Cast extension.
|
|
||||||
|
|
||||||
# DefaultCastOptionsProvider is commonly referred to only by the app's manifest.
|
|
||||||
-keep class com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider
|
|
||||||
|
|
@ -15,9 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.os.Looper;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.NonNull;
|
||||||
import android.util.Log;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.BasePlayer;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
|
@ -29,8 +30,8 @@ import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
|
||||||
import com.google.android.gms.cast.CastStatusCodes;
|
import com.google.android.gms.cast.CastStatusCodes;
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
import com.google.android.gms.cast.MediaInfo;
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
import com.google.android.gms.cast.MediaQueueItem;
|
||||||
|
|
@ -44,41 +45,27 @@ 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.
|
||||||
*
|
*
|
||||||
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
* <p>The behavior of this class depends on the underlying Cast session, which is obtained from the
|
||||||
* Cast context passed to {@link #CastPlayer}. To keep track of the session,
|
* Cast context passed to {@link #CastPlayer}. To keep track of the session, {@link
|
||||||
* {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
* #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be
|
||||||
* implemented and attached to the player.</p>
|
* implemented and attached to the player.
|
||||||
*
|
*
|
||||||
* <p>If no session is available, the player state will remain unchanged and calls to methods that
|
* <p>If no session is available, the player state will remain unchanged and calls to methods that
|
||||||
* alter it will be ignored. Querying the player state is possible even when no session is
|
* alter it will be ignored. Querying the player state is possible even when no session is
|
||||||
* available, in which case, the last observed receiver app state is reported.</p>
|
* available, in which case, the last observed receiver app state is reported.
|
||||||
*
|
*
|
||||||
* <p>Methods should be called on the application's main thread.</p>
|
* <p>Methods should be called on the application's main thread.
|
||||||
*/
|
*/
|
||||||
public final class CastPlayer implements Player {
|
public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener of changes in the cast session availability.
|
|
||||||
*/
|
|
||||||
public interface SessionAvailabilityListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a cast session becomes available to the player.
|
|
||||||
*/
|
|
||||||
void onCastSessionAvailable();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the cast session becomes unavailable.
|
|
||||||
*/
|
|
||||||
void onCastSessionUnavailable();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String TAG = "CastPlayer";
|
private static final String TAG = "CastPlayer";
|
||||||
|
|
||||||
|
|
@ -94,24 +81,24 @@ public final class CastPlayer implements Player {
|
||||||
private final CastContext castContext;
|
private final CastContext castContext;
|
||||||
// TODO: Allow custom implementations of CastTimelineTracker.
|
// TODO: Allow custom implementations of CastTimelineTracker.
|
||||||
private final CastTimelineTracker timelineTracker;
|
private final CastTimelineTracker timelineTracker;
|
||||||
private final Timeline.Window window;
|
|
||||||
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;
|
||||||
private int playbackState;
|
@Player.State private int playbackState;
|
||||||
private int repeatMode;
|
private int repeatMode;
|
||||||
private int currentWindowIndex;
|
private int currentWindowIndex;
|
||||||
private boolean playWhenReady;
|
private boolean playWhenReady;
|
||||||
|
|
@ -127,11 +114,12 @@ public final class CastPlayer implements Player {
|
||||||
public CastPlayer(CastContext castContext) {
|
public CastPlayer(CastContext castContext) {
|
||||||
this.castContext = castContext;
|
this.castContext = castContext;
|
||||||
timelineTracker = new CastTimelineTracker();
|
timelineTracker = new CastTimelineTracker();
|
||||||
window = new Timeline.Window();
|
|
||||||
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);
|
||||||
|
|
@ -159,6 +147,7 @@ public final class CastPlayer implements Player {
|
||||||
* 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);
|
||||||
}
|
}
|
||||||
|
|
@ -174,8 +163,9 @@ public final class CastPlayer implements Player {
|
||||||
* @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;
|
||||||
|
|
@ -191,6 +181,7 @@ public final class CastPlayer implements Player {
|
||||||
* @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);
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +196,7 @@ public final class CastPlayer implements Player {
|
||||||
* @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)) {
|
||||||
|
|
@ -222,6 +214,7 @@ public final class CastPlayer implements Player {
|
||||||
* @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);
|
||||||
|
|
@ -240,6 +233,7 @@ public final class CastPlayer implements Player {
|
||||||
* @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) {
|
||||||
|
|
@ -257,6 +251,7 @@ public final class CastPlayer implements Player {
|
||||||
* @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
|
||||||
|
|
@ -275,45 +270,66 @@ public final class CastPlayer implements Player {
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player implementation.
|
// Player implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public AudioComponent getAudioComponent() {
|
public AudioComponent getAudioComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public VideoComponent getVideoComponent() {
|
public VideoComponent getVideoComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public TextComponent getTextComponent() {
|
public TextComponent getTextComponent() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public MetadataComponent getMetadataComponent() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Looper getApplicationLooper() {
|
||||||
|
return Looper.getMainLooper();
|
||||||
|
}
|
||||||
|
|
||||||
@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
|
||||||
|
@Player.State
|
||||||
public int getPlaybackState() {
|
public int getPlaybackState() {
|
||||||
return playbackState;
|
return playbackState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nullable
|
||||||
public ExoPlaybackException getPlaybackError() {
|
public ExoPlaybackException getPlaybackError() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -335,21 +351,6 @@ public final class CastPlayer implements Player {
|
||||||
return playWhenReady;
|
return playWhenReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void seekToDefaultPosition() {
|
|
||||||
seekTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void seekToDefaultPosition(int windowIndex) {
|
|
||||||
seekTo(windowIndex, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void seekTo(long positionMs) {
|
|
||||||
seekTo(getCurrentWindowIndex(), positionMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seekTo(int windowIndex, long positionMs) {
|
public void seekTo(int windowIndex, long positionMs) {
|
||||||
MediaStatus mediaStatus = getMediaStatus();
|
MediaStatus mediaStatus = getMediaStatus();
|
||||||
|
|
@ -366,14 +367,13 @@ public final class CastPlayer implements Player {
|
||||||
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
|
||||||
|
|
@ -386,11 +386,6 @@ public final class CastPlayer implements Player {
|
||||||
return PlaybackParameters.DEFAULT;
|
return PlaybackParameters.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
stop(/* reset= */ false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop(boolean reset) {
|
public void stop(boolean reset) {
|
||||||
playbackState = STATE_IDLE;
|
playbackState = STATE_IDLE;
|
||||||
|
|
@ -465,11 +460,6 @@ public final class CastPlayer implements Player {
|
||||||
return currentTimeline;
|
return currentTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable public Object getCurrentManifest() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCurrentPeriodIndex() {
|
public int getCurrentPeriodIndex() {
|
||||||
return getCurrentWindowIndex();
|
return getCurrentWindowIndex();
|
||||||
|
|
@ -480,32 +470,11 @@ public final class CastPlayer implements Player {
|
||||||
return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
|
return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getNextWindowIndex() {
|
|
||||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
|
||||||
: currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPreviousWindowIndex() {
|
|
||||||
return currentTimeline.isEmpty() ? C.INDEX_UNSET
|
|
||||||
: currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @Nullable Object getCurrentTag() {
|
|
||||||
int windowIndex = getCurrentWindowIndex();
|
|
||||||
return windowIndex > currentTimeline.getWindowCount()
|
|
||||||
? null
|
|
||||||
: currentTimeline.getWindow(windowIndex, window, /* setTag= */ true).tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
|
// TODO: Fill the cast timeline information with ProgressListener's duration updates.
|
||||||
// See [Internal: b/65152553].
|
// See [Internal: b/65152553].
|
||||||
@Override
|
@Override
|
||||||
public long getDuration() {
|
public long getDuration() {
|
||||||
return currentTimeline.isEmpty() ? C.TIME_UNSET
|
return getContentDuration();
|
||||||
: currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -522,15 +491,6 @@ public final class CastPlayer implements Player {
|
||||||
return getCurrentPosition();
|
return getCurrentPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getBufferedPercentage() {
|
|
||||||
long position = getBufferedPosition();
|
|
||||||
long duration = getDuration();
|
|
||||||
return position == C.TIME_UNSET || duration == C.TIME_UNSET
|
|
||||||
? 0
|
|
||||||
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getTotalBufferedDuration() {
|
public long getTotalBufferedDuration() {
|
||||||
long bufferedPosition = getBufferedPosition();
|
long bufferedPosition = getBufferedPosition();
|
||||||
|
|
@ -540,18 +500,6 @@ public final class CastPlayer implements Player {
|
||||||
: bufferedPosition - currentPosition;
|
: bufferedPosition - currentPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCurrentWindowDynamic() {
|
|
||||||
return !currentTimeline.isEmpty()
|
|
||||||
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCurrentWindowSeekable() {
|
|
||||||
return !currentTimeline.isEmpty()
|
|
||||||
&& currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPlayingAd() {
|
public boolean isPlayingAd() {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -567,11 +515,6 @@ public final class CastPlayer implements Player {
|
||||||
return C.INDEX_UNSET;
|
return C.INDEX_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getContentDuration() {
|
|
||||||
return getDuration();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLoading() {
|
public boolean isLoading() {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -589,7 +532,7 @@ public final class CastPlayer implements Player {
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
@ -601,30 +544,40 @@ public final class CastPlayer implements Player {
|
||||||
|| 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() {
|
||||||
|
|
@ -632,9 +585,9 @@ public final class CastPlayer implements Player {
|
||||||
@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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -645,7 +598,9 @@ public final class CastPlayer implements Player {
|
||||||
CastTimeline oldTimeline = currentTimeline;
|
CastTimeline oldTimeline = currentTimeline;
|
||||||
MediaStatus status = getMediaStatus();
|
MediaStatus status = getMediaStatus();
|
||||||
currentTimeline =
|
currentTimeline =
|
||||||
status != null ? timelineTracker.getCastTimeline(status) : CastTimeline.EMPTY_CAST_TIMELINE;
|
status != null
|
||||||
|
? timelineTracker.getCastTimeline(remoteMediaClient)
|
||||||
|
: CastTimeline.EMPTY_CAST_TIMELINE;
|
||||||
return !oldTimeline.equals(currentTimeline);
|
return !oldTimeline.equals(currentTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -722,7 +677,8 @@ public final class CastPlayer implements Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable MediaStatus getMediaStatus() {
|
@Nullable
|
||||||
|
private MediaStatus getMediaStatus() {
|
||||||
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -770,16 +726,6 @@ public final class CastPlayer implements Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
||||||
|
|
@ -895,7 +841,23 @@ public final class CastPlayer implements Player {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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> {
|
||||||
|
|
||||||
|
|
@ -909,11 +871,27 @@ public final class CastPlayer implements Player {
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,66 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import android.util.SparseArray;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link Timeline} for Cast media queues.
|
* A {@link Timeline} for Cast media queues.
|
||||||
*/
|
*/
|
||||||
/* package */ final class CastTimeline extends Timeline {
|
/* package */ final class CastTimeline extends Timeline {
|
||||||
|
|
||||||
|
/** Holds {@link Timeline} related data for a Cast media item. */
|
||||||
|
public static final class ItemData {
|
||||||
|
|
||||||
|
/** Holds no media information. */
|
||||||
|
public static final ItemData EMPTY = new ItemData();
|
||||||
|
|
||||||
|
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
|
||||||
|
public final long durationUs;
|
||||||
|
/**
|
||||||
|
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
|
||||||
|
*/
|
||||||
|
public final long defaultPositionUs;
|
||||||
|
|
||||||
|
private ItemData() {
|
||||||
|
this(/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param durationUs See {@link #durationsUs}.
|
||||||
|
* @param defaultPositionUs See {@link #defaultPositionUs}.
|
||||||
|
*/
|
||||||
|
public ItemData(long durationUs, long defaultPositionUs) {
|
||||||
|
this.durationUs = durationUs;
|
||||||
|
this.defaultPositionUs = defaultPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns an instance with the given {@link #durationsUs}. */
|
||||||
|
public ItemData copyWithDurationUs(long durationUs) {
|
||||||
|
if (durationUs == this.durationUs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new ItemData(durationUs, defaultPositionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns an instance with the given {@link #defaultPositionsUs}. */
|
||||||
|
public ItemData copyWithDefaultPositionUs(long defaultPositionUs) {
|
||||||
|
if (defaultPositionUs == this.defaultPositionUs) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new ItemData(durationUs, defaultPositionUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link Timeline} for a cast queue that has no items. */
|
||||||
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
public static final CastTimeline EMPTY_CAST_TIMELINE =
|
||||||
new CastTimeline(Collections.emptyList(), Collections.emptyMap());
|
new CastTimeline(new int[0], new SparseArray<>());
|
||||||
|
|
||||||
private final SparseIntArray idsToIndex;
|
private final SparseIntArray idsToIndex;
|
||||||
private final int[] ids;
|
private final int[] ids;
|
||||||
|
|
@ -40,28 +82,23 @@ import java.util.Map;
|
||||||
private final long[] defaultPositionsUs;
|
private final long[] defaultPositionsUs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param items A list of cast media queue items to represent.
|
* Creates a Cast timeline from the given data.
|
||||||
* @param contentIdToDurationUsMap A map of content id to duration in microseconds.
|
*
|
||||||
|
* @param itemIds The ids of the items in the timeline.
|
||||||
|
* @param itemIdToData Maps item ids to {@link ItemData}.
|
||||||
*/
|
*/
|
||||||
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) {
|
public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
|
||||||
int itemCount = items.size();
|
int itemCount = itemIds.length;
|
||||||
int index = 0;
|
|
||||||
idsToIndex = new SparseIntArray(itemCount);
|
idsToIndex = new SparseIntArray(itemCount);
|
||||||
ids = new int[itemCount];
|
ids = Arrays.copyOf(itemIds, itemCount);
|
||||||
durationsUs = new long[itemCount];
|
durationsUs = new long[itemCount];
|
||||||
defaultPositionsUs = new long[itemCount];
|
defaultPositionsUs = new long[itemCount];
|
||||||
for (MediaQueueItem item : items) {
|
for (int i = 0; i < ids.length; i++) {
|
||||||
int itemId = item.getItemId();
|
int id = ids[i];
|
||||||
ids[index] = itemId;
|
idsToIndex.put(id, i);
|
||||||
idsToIndex.put(itemId, index);
|
ItemData data = itemIdToData.get(id, ItemData.EMPTY);
|
||||||
MediaInfo mediaInfo = item.getMedia();
|
durationsUs[i] = data.durationUs;
|
||||||
String contentId = mediaInfo.getContentId();
|
defaultPositionsUs[i] = data.defaultPositionUs;
|
||||||
durationsUs[index] =
|
|
||||||
contentIdToDurationUsMap.containsKey(contentId)
|
|
||||||
? contentIdToDurationUsMap.get(contentId)
|
|
||||||
: CastUtils.getStreamDurationUs(mediaInfo);
|
|
||||||
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
|
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +117,7 @@ import java.util.Map;
|
||||||
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,
|
||||||
|
|
@ -108,7 +146,7 @@ import java.util.Map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getUidOfPeriod(int periodIndex) {
|
public Integer getUidOfPeriod(int periodIndex) {
|
||||||
return ids[periodIndex];
|
return ids[periodIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,53 +15,84 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
import android.util.SparseArray;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
import com.google.android.gms.cast.MediaQueueItem;
|
||||||
import com.google.android.gms.cast.MediaStatus;
|
import com.google.android.gms.cast.MediaStatus;
|
||||||
import java.util.HashMap;
|
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates {@link CastTimeline}s from cast receiver app media status.
|
* Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
|
||||||
*
|
*
|
||||||
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
* <p>This class keeps track of the duration reported by the current item to fill any missing
|
||||||
* durations in the media queue items [See internal: b/65152553].
|
* durations in the media queue items [See internal: b/65152553].
|
||||||
*/
|
*/
|
||||||
/* package */ final class CastTimelineTracker {
|
/* package */ final class CastTimelineTracker {
|
||||||
|
|
||||||
private final HashMap<String, Long> contentIdToDurationUsMap;
|
private final SparseArray<CastTimeline.ItemData> itemIdToData;
|
||||||
private final HashSet<String> scratchContentIdSet;
|
|
||||||
|
|
||||||
public CastTimelineTracker() {
|
public CastTimelineTracker() {
|
||||||
contentIdToDurationUsMap = new HashMap<>();
|
itemIdToData = new SparseArray<>();
|
||||||
scratchContentIdSet = new HashSet<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link CastTimeline} that represent the given {@code status}.
|
* Returns a {@link CastTimeline} that represents the state of the given {@code
|
||||||
|
* remoteMediaClient}.
|
||||||
*
|
*
|
||||||
* @param status The Cast media status.
|
* <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
|
||||||
* @return A {@link CastTimeline} that represent the given {@code status}.
|
* invocations of this method.
|
||||||
|
*
|
||||||
|
* @param remoteMediaClient The Cast media client.
|
||||||
|
* @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
|
||||||
*/
|
*/
|
||||||
public CastTimeline getCastTimeline(MediaStatus status) {
|
public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
|
||||||
MediaInfo mediaInfo = status.getMediaInfo();
|
int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
|
||||||
List<MediaQueueItem> items = status.getQueueItems();
|
if (itemIds.length > 0) {
|
||||||
removeUnusedDurationEntries(items);
|
// Only remove unused items when there is something in the queue to avoid removing all entries
|
||||||
|
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
|
||||||
if (mediaInfo != null) {
|
removeUnusedItemDataEntries(itemIds);
|
||||||
String contentId = mediaInfo.getContentId();
|
|
||||||
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
|
|
||||||
contentIdToDurationUsMap.put(contentId, durationUs);
|
|
||||||
}
|
|
||||||
return new CastTimeline(items, contentIdToDurationUsMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeUnusedDurationEntries(List<MediaQueueItem> items) {
|
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
|
||||||
scratchContentIdSet.clear();
|
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
|
||||||
for (MediaQueueItem item : items) {
|
if (mediaStatus == null) {
|
||||||
scratchContentIdSet.add(item.getMedia().getContentId());
|
return CastTimeline.EMPTY_CAST_TIMELINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentItemId = mediaStatus.getCurrentItemId();
|
||||||
|
long durationUs = CastUtils.getStreamDurationUs(mediaStatus.getMediaInfo());
|
||||||
|
itemIdToData.put(
|
||||||
|
currentItemId,
|
||||||
|
itemIdToData
|
||||||
|
.get(currentItemId, CastTimeline.ItemData.EMPTY)
|
||||||
|
.copyWithDurationUs(durationUs));
|
||||||
|
|
||||||
|
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
|
||||||
|
int itemId = item.getItemId();
|
||||||
|
itemIdToData.put(
|
||||||
|
itemId,
|
||||||
|
itemIdToData
|
||||||
|
.get(itemId, CastTimeline.ItemData.EMPTY)
|
||||||
|
.copyWithDefaultPositionUs((long) (item.getStartTime() * C.MICROS_PER_SECOND)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CastTimeline(itemIds, itemIdToData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeUnusedItemDataEntries(int[] itemIds) {
|
||||||
|
HashSet<Integer> scratchItemIds = new HashSet<>(/* initialCapacity= */ itemIds.length * 2);
|
||||||
|
for (int id : itemIds) {
|
||||||
|
scratchItemIds.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (index < itemIdToData.size()) {
|
||||||
|
if (!scratchItemIds.contains(itemIdToData.keyAt(index))) {
|
||||||
|
itemIdToData.removeAt(index);
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.gms.cast.CastStatusCodes;
|
import com.google.android.gms.cast.CastStatusCodes;
|
||||||
|
|
@ -31,11 +32,13 @@ import com.google.android.gms.cast.MediaTrack;
|
||||||
* unknown or not applicable.
|
* unknown or not applicable.
|
||||||
*
|
*
|
||||||
* @param mediaInfo The media info to get the duration from.
|
* @param mediaInfo The media info to get the duration from.
|
||||||
* @return The duration in microseconds.
|
* @return The duration in microseconds, or {@link C#TIME_UNSET} if unknown or not applicable.
|
||||||
*/
|
*/
|
||||||
public static long getStreamDurationUs(MediaInfo mediaInfo) {
|
public static long getStreamDurationUs(@Nullable MediaInfo mediaInfo) {
|
||||||
long durationMs =
|
if (mediaInfo == null) {
|
||||||
mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
|
return C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
long durationMs = mediaInfo.getStreamDuration();
|
||||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +112,7 @@ import com.google.android.gms.cast.MediaTrack;
|
||||||
/* codecs= */ null,
|
/* codecs= */ null,
|
||||||
/* bitrate= */ Format.NO_VALUE,
|
/* bitrate= */ Format.NO_VALUE,
|
||||||
/* selectionFlags= */ 0,
|
/* selectionFlags= */ 0,
|
||||||
|
/* roleFlags= */ 0,
|
||||||
mediaTrack.getLanguage());
|
mediaTrack.getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.google.android.gms.cast.CastMediaControlIntent;
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
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.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Representation of a media item. */
|
||||||
|
public final class MediaItem {
|
||||||
|
|
||||||
|
/** A builder for {@link MediaItem} instances. */
|
||||||
|
public static final class Builder {
|
||||||
|
|
||||||
|
@Nullable private Uri uri;
|
||||||
|
@Nullable private String title;
|
||||||
|
@Nullable private String mimeType;
|
||||||
|
@Nullable private DrmConfiguration drmConfiguration;
|
||||||
|
|
||||||
|
/** See {@link MediaItem#uri}. */
|
||||||
|
public Builder setUri(String uri) {
|
||||||
|
return setUri(Uri.parse(uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See {@link MediaItem#uri}. */
|
||||||
|
public Builder setUri(Uri uri) {
|
||||||
|
this.uri = uri;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See {@link MediaItem#title}. */
|
||||||
|
public Builder setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See {@link MediaItem#mimeType}. */
|
||||||
|
public Builder setMimeType(String mimeType) {
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See {@link MediaItem#drmConfiguration}. */
|
||||||
|
public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
|
||||||
|
this.drmConfiguration = drmConfiguration;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a new {@link MediaItem} instance with the current builder values. */
|
||||||
|
public MediaItem build() {
|
||||||
|
Assertions.checkNotNull(uri);
|
||||||
|
return new MediaItem(uri, title, mimeType, drmConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DRM configuration for a media item. */
|
||||||
|
public static final class DrmConfiguration {
|
||||||
|
|
||||||
|
/** The UUID of the protection scheme. */
|
||||||
|
public final UUID uuid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional license server {@link Uri}. If {@code null} then the license server must be
|
||||||
|
* specified by the media.
|
||||||
|
*/
|
||||||
|
@Nullable public final Uri licenseUri;
|
||||||
|
|
||||||
|
/** Headers that should be attached to any license requests. */
|
||||||
|
public final Map<String, String> requestHeaders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param uuid See {@link #uuid}.
|
||||||
|
* @param licenseUri See {@link #licenseUri}.
|
||||||
|
* @param requestHeaders See {@link #requestHeaders}.
|
||||||
|
*/
|
||||||
|
public DrmConfiguration(
|
||||||
|
UUID uuid, @Nullable Uri licenseUri, @Nullable Map<String, String> requestHeaders) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.licenseUri = licenseUri;
|
||||||
|
this.requestHeaders =
|
||||||
|
requestHeaders == null
|
||||||
|
? Collections.emptyMap()
|
||||||
|
: Collections.unmodifiableMap(requestHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrmConfiguration other = (DrmConfiguration) obj;
|
||||||
|
return uuid.equals(other.uuid)
|
||||||
|
&& Util.areEqual(licenseUri, other.licenseUri)
|
||||||
|
&& requestHeaders.equals(other.requestHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = uuid.hashCode();
|
||||||
|
result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
|
||||||
|
result = 31 * result + requestHeaders.hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The media {@link Uri}. */
|
||||||
|
public final Uri uri;
|
||||||
|
|
||||||
|
/** The title of the item, or {@code null} if unspecified. */
|
||||||
|
@Nullable public final String title;
|
||||||
|
|
||||||
|
/** The mime type for the media, or {@code null} if unspecified. */
|
||||||
|
@Nullable public final String mimeType;
|
||||||
|
|
||||||
|
/** Optional {@link DrmConfiguration} for the media. */
|
||||||
|
@Nullable public final DrmConfiguration drmConfiguration;
|
||||||
|
|
||||||
|
private MediaItem(
|
||||||
|
Uri uri,
|
||||||
|
@Nullable String title,
|
||||||
|
@Nullable String mimeType,
|
||||||
|
@Nullable DrmConfiguration drmConfiguration) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.title = title;
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
this.drmConfiguration = drmConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MediaItem other = (MediaItem) obj;
|
||||||
|
return uri.equals(other.uri)
|
||||||
|
&& Util.areEqual(title, other.title)
|
||||||
|
&& Util.areEqual(mimeType, other.mimeType)
|
||||||
|
&& Util.areEqual(drmConfiguration, other.drmConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = uri.hashCode();
|
||||||
|
result = 31 * result + (title == null ? 0 : title.hashCode());
|
||||||
|
result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
|
||||||
|
result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/** Listener of changes in the cast session availability. */
|
||||||
|
public interface SessionAvailabilityListener {
|
||||||
|
|
||||||
|
/** Called when a cast session becomes available to the player. */
|
||||||
|
void onCastSessionAvailable();
|
||||||
|
|
||||||
|
/** Called when the cast session becomes unavailable. */
|
||||||
|
void onCastSessionUnavailable();
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,23 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
import com.google.android.exoplayer2.testutil.TimelineAsserts;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.gms.cast.MediaInfo;
|
import com.google.android.gms.cast.MediaInfo;
|
||||||
import com.google.android.gms.cast.MediaQueueItem;
|
|
||||||
import com.google.android.gms.cast.MediaStatus;
|
import com.google.android.gms.cast.MediaStatus;
|
||||||
import java.util.ArrayList;
|
import com.google.android.gms.cast.framework.media.MediaQueue;
|
||||||
|
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||||
|
import java.util.Collections;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
|
|
||||||
/** Tests for {@link CastTimelineTracker}. */
|
/** Tests for {@link CastTimelineTracker}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class CastTimelineTrackerTest {
|
public class CastTimelineTrackerTest {
|
||||||
|
|
||||||
private static final long DURATION_1_MS = 1000;
|
|
||||||
private static final long DURATION_2_MS = 2000;
|
private static final long DURATION_2_MS = 2000;
|
||||||
private static final long DURATION_3_MS = 3000;
|
private static final long DURATION_3_MS = 3000;
|
||||||
private static final long DURATION_4_MS = 4000;
|
private static final long DURATION_4_MS = 4000;
|
||||||
|
|
@ -39,91 +39,89 @@ public class CastTimelineTrackerTest {
|
||||||
|
|
||||||
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
/** Tests that duration of the current media info is correctly propagated to the timeline. */
|
||||||
@Test
|
@Test
|
||||||
public void testGetCastTimeline() {
|
public void testGetCastTimelinePersistsDuration() {
|
||||||
MediaInfo mediaInfo;
|
|
||||||
MediaStatus status =
|
|
||||||
mockMediaStatus(
|
|
||||||
new int[] {1, 2, 3},
|
|
||||||
new String[] {"contentId1", "contentId2", "contentId3"},
|
|
||||||
new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
|
|
||||||
|
|
||||||
CastTimelineTracker tracker = new CastTimelineTracker();
|
CastTimelineTracker tracker = new CastTimelineTracker();
|
||||||
mediaInfo = getMediaInfo("contentId1", DURATION_1_MS);
|
|
||||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
|
||||||
TimelineAsserts.assertPeriodDurations(
|
|
||||||
tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
|
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
|
RemoteMediaClient remoteMediaClient =
|
||||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
|
||||||
|
/* currentItemId= */ 2,
|
||||||
|
/* currentDurationMs= */ DURATION_2_MS);
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(status),
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
C.msToUs(DURATION_1_MS),
|
|
||||||
C.TIME_UNSET,
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_3_MS));
|
C.msToUs(DURATION_2_MS),
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.TIME_UNSET);
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId2", DURATION_2_MS);
|
remoteMediaClient =
|
||||||
Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3},
|
||||||
|
/* currentItemId= */ 3,
|
||||||
|
/* currentDurationMs= */ DURATION_3_MS);
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(status),
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
C.msToUs(DURATION_1_MS),
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_2_MS),
|
C.msToUs(DURATION_2_MS),
|
||||||
C.msToUs(DURATION_3_MS));
|
C.msToUs(DURATION_3_MS));
|
||||||
|
|
||||||
MediaStatus newStatus =
|
remoteMediaClient =
|
||||||
mockMediaStatus(
|
mockRemoteMediaClient(
|
||||||
new int[] {4, 1, 5, 3},
|
/* itemIds= */ new int[] {1, 3},
|
||||||
new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
|
/* currentItemId= */ 3,
|
||||||
new long[] {
|
/* currentDurationMs= */ DURATION_3_MS);
|
||||||
MediaInfo.UNKNOWN_DURATION,
|
|
||||||
MediaInfo.UNKNOWN_DURATION,
|
|
||||||
DURATION_5_MS,
|
|
||||||
MediaInfo.UNKNOWN_DURATION
|
|
||||||
});
|
|
||||||
mediaInfo = getMediaInfo("contentId5", DURATION_5_MS);
|
|
||||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(newStatus),
|
tracker.getCastTimeline(remoteMediaClient), C.TIME_UNSET, C.msToUs(DURATION_3_MS));
|
||||||
C.TIME_UNSET,
|
|
||||||
C.msToUs(DURATION_1_MS),
|
|
||||||
C.msToUs(DURATION_5_MS),
|
|
||||||
C.msToUs(DURATION_3_MS));
|
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId3", DURATION_3_MS);
|
remoteMediaClient =
|
||||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
|
||||||
|
/* currentItemId= */ 4,
|
||||||
|
/* currentDurationMs= */ DURATION_4_MS);
|
||||||
TimelineAsserts.assertPeriodDurations(
|
TimelineAsserts.assertPeriodDurations(
|
||||||
tracker.getCastTimeline(newStatus),
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
C.TIME_UNSET,
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_1_MS),
|
C.TIME_UNSET,
|
||||||
C.msToUs(DURATION_5_MS),
|
C.msToUs(DURATION_3_MS),
|
||||||
C.msToUs(DURATION_3_MS));
|
|
||||||
|
|
||||||
mediaInfo = getMediaInfo("contentId4", DURATION_4_MS);
|
|
||||||
Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
|
|
||||||
TimelineAsserts.assertPeriodDurations(
|
|
||||||
tracker.getCastTimeline(newStatus),
|
|
||||||
C.msToUs(DURATION_4_MS),
|
C.msToUs(DURATION_4_MS),
|
||||||
C.msToUs(DURATION_1_MS),
|
C.TIME_UNSET);
|
||||||
C.msToUs(DURATION_5_MS),
|
|
||||||
C.msToUs(DURATION_3_MS));
|
remoteMediaClient =
|
||||||
|
mockRemoteMediaClient(
|
||||||
|
/* itemIds= */ new int[] {1, 2, 3, 4, 5},
|
||||||
|
/* currentItemId= */ 5,
|
||||||
|
/* currentDurationMs= */ DURATION_5_MS);
|
||||||
|
TimelineAsserts.assertPeriodDurations(
|
||||||
|
tracker.getCastTimeline(remoteMediaClient),
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.TIME_UNSET,
|
||||||
|
C.msToUs(DURATION_3_MS),
|
||||||
|
C.msToUs(DURATION_4_MS),
|
||||||
|
C.msToUs(DURATION_5_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaStatus mockMediaStatus(
|
private static RemoteMediaClient mockRemoteMediaClient(
|
||||||
int[] itemIds, String[] contentIds, long[] durationsMs) {
|
int[] itemIds, int currentItemId, long currentDurationMs) {
|
||||||
ArrayList<MediaQueueItem> items = new ArrayList<>();
|
RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class);
|
||||||
for (int i = 0; i < contentIds.length; i++) {
|
|
||||||
MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]);
|
|
||||||
MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
|
|
||||||
Mockito.when(item.getMedia()).thenReturn(mediaInfo);
|
|
||||||
Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
|
|
||||||
items.add(item);
|
|
||||||
}
|
|
||||||
MediaStatus status = Mockito.mock(MediaStatus.class);
|
MediaStatus status = Mockito.mock(MediaStatus.class);
|
||||||
Mockito.when(status.getQueueItems()).thenReturn(items);
|
Mockito.when(status.getQueueItems()).thenReturn(Collections.emptyList());
|
||||||
return status;
|
Mockito.when(remoteMediaClient.getMediaStatus()).thenReturn(status);
|
||||||
|
Mockito.when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs));
|
||||||
|
Mockito.when(status.getCurrentItemId()).thenReturn(currentItemId);
|
||||||
|
MediaQueue mediaQueue = mockMediaQueue(itemIds);
|
||||||
|
Mockito.when(remoteMediaClient.getMediaQueue()).thenReturn(mediaQueue);
|
||||||
|
return remoteMediaClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaInfo getMediaInfo(String contentId, long durationMs) {
|
private static MediaQueue mockMediaQueue(int[] itemIds) {
|
||||||
return new MediaInfo.Builder(contentId)
|
MediaQueue mediaQueue = Mockito.mock(MediaQueue.class);
|
||||||
|
Mockito.when(mediaQueue.getItemIds()).thenReturn(itemIds);
|
||||||
|
return mediaQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaInfo getMediaInfo(long durationMs) {
|
||||||
|
return new MediaInfo.Builder(/*contentId= */ "")
|
||||||
.setStreamDuration(durationMs)
|
.setStreamDuration(durationMs)
|
||||||
.setContentType(MimeTypes.APPLICATION_MP4)
|
.setContentType(MimeTypes.APPLICATION_MP4)
|
||||||
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
|
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
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.util.MimeTypes;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Test for {@link MediaItem}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class MediaItemTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildMediaItem_doesNotChangeState() {
|
||||||
|
MediaItem.Builder builder = new MediaItem.Builder();
|
||||||
|
MediaItem item1 =
|
||||||
|
builder
|
||||||
|
.setUri(Uri.parse("http://example.com"))
|
||||||
|
.setTitle("title")
|
||||||
|
.setMimeType(MimeTypes.AUDIO_MP4)
|
||||||
|
.build();
|
||||||
|
MediaItem item2 = builder.build();
|
||||||
|
assertThat(item1).isEqualTo(item2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void equals_withEqualDrmSchemes_returnsTrue() {
|
||||||
|
MediaItem.Builder builder1 = new MediaItem.Builder();
|
||||||
|
MediaItem mediaItem1 =
|
||||||
|
builder1
|
||||||
|
.setUri(Uri.parse("www.google.com"))
|
||||||
|
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||||
|
.build();
|
||||||
|
MediaItem.Builder builder2 = new MediaItem.Builder();
|
||||||
|
MediaItem mediaItem2 =
|
||||||
|
builder2
|
||||||
|
.setUri(Uri.parse("www.google.com"))
|
||||||
|
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||||
|
.build();
|
||||||
|
assertThat(mediaItem1).isEqualTo(mediaItem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
|
||||||
|
MediaItem.Builder builder1 = new MediaItem.Builder();
|
||||||
|
MediaItem mediaItem1 =
|
||||||
|
builder1
|
||||||
|
.setUri(Uri.parse("www.google.com"))
|
||||||
|
.setDrmConfiguration(buildDrmConfiguration(1))
|
||||||
|
.build();
|
||||||
|
MediaItem.Builder builder2 = new MediaItem.Builder();
|
||||||
|
MediaItem mediaItem2 =
|
||||||
|
builder2
|
||||||
|
.setUri(Uri.parse("www.google.com"))
|
||||||
|
.setDrmConfiguration(buildDrmConfiguration(2))
|
||||||
|
.build();
|
||||||
|
assertThat(mediaItem1).isNotEqualTo(mediaItem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
|
||||||
|
HashMap<String, String> requestHeaders = new HashMap<>();
|
||||||
|
requestHeaders.put("key1", "value1");
|
||||||
|
requestHeaders.put("key2", "value2" + seed);
|
||||||
|
return new MediaItem.DrmConfiguration(
|
||||||
|
C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
manifest=src/test/AndroidManifest.xml
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
|
The Cronet extension is an [HttpDataSource][] implementation using [Cronet][].
|
||||||
|
|
||||||
[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
[HttpDataSource]: https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html
|
||||||
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
|
[Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F
|
||||||
|
|
||||||
## Getting the extension ##
|
## Getting the extension ##
|
||||||
|
|
@ -52,4 +52,4 @@ respectively.
|
||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,9 @@ apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion project.ext.minSdkVersion
|
||||||
targetSdkVersion project.ext.targetSdkVersion
|
targetSdkVersion project.ext.targetSdkVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,14 +26,18 @@ android {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.chromium.net:cronet-embedded:66.3359.158'
|
api 'org.chromium.net:cronet-embedded:75.3770.101'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
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,11 +15,14 @@
|
||||||
*/
|
*/
|
||||||
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 android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
|
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
|
||||||
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
import com.google.android.exoplayer2.upstream.BaseDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
import com.google.android.exoplayer2.upstream.DataSourceException;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
|
|
@ -27,6 +30,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Predicate;
|
import com.google.android.exoplayer2.util.Predicate;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
|
|
@ -39,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;
|
||||||
|
|
@ -111,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;
|
||||||
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;
|
||||||
|
|
@ -128,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;
|
||||||
|
|
@ -151,21 +157,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
* 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
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
* handling is a fast operation when using a direct executor.
|
* handling is a fast operation when using a direct executor.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* #open(DataSpec)}.
|
|
||||||
*/
|
*/
|
||||||
public CronetDataSource(
|
public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
|
||||||
CronetEngine cronetEngine, Executor executor, Predicate<String> contentTypePredicate) {
|
|
||||||
this(
|
this(
|
||||||
cronetEngine,
|
cronetEngine,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||||
DEFAULT_READ_TIMEOUT_MILLIS,
|
DEFAULT_READ_TIMEOUT_MILLIS,
|
||||||
false,
|
/* resetTimeoutOnRedirects= */ false,
|
||||||
null,
|
/* defaultRequestProperties= */ null);
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -175,32 +175,28 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
* 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
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
* handling is a fast operation when using a direct executor.
|
* handling is a fast operation when using a direct executor.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* #open(DataSpec)}.
|
|
||||||
* @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.
|
||||||
*/
|
*/
|
||||||
public CronetDataSource(
|
public CronetDataSource(
|
||||||
CronetEngine cronetEngine,
|
CronetEngine cronetEngine,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
int connectTimeoutMs,
|
int connectTimeoutMs,
|
||||||
int readTimeoutMs,
|
int readTimeoutMs,
|
||||||
boolean resetTimeoutOnRedirects,
|
boolean resetTimeoutOnRedirects,
|
||||||
RequestProperties defaultRequestProperties) {
|
@Nullable RequestProperties defaultRequestProperties) {
|
||||||
this(
|
this(
|
||||||
cronetEngine,
|
cronetEngine,
|
||||||
executor,
|
executor,
|
||||||
contentTypePredicate,
|
|
||||||
connectTimeoutMs,
|
connectTimeoutMs,
|
||||||
readTimeoutMs,
|
readTimeoutMs,
|
||||||
resetTimeoutOnRedirects,
|
resetTimeoutOnRedirects,
|
||||||
Clock.DEFAULT,
|
Clock.DEFAULT,
|
||||||
defaultRequestProperties,
|
defaultRequestProperties,
|
||||||
false);
|
/* handleSetCookieRequests= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -210,29 +206,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
* 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
|
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||||
* handling is a fast operation when using a direct executor.
|
* handling is a fast operation when using a direct executor.
|
||||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
|
||||||
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
|
||||||
* #open(DataSpec)}.
|
|
||||||
* @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.
|
||||||
*/
|
*/
|
||||||
public CronetDataSource(
|
public CronetDataSource(
|
||||||
CronetEngine cronetEngine,
|
CronetEngine cronetEngine,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
Predicate<String> contentTypePredicate,
|
|
||||||
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,
|
||||||
|
|
@ -241,21 +233,127 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
handleSetCookieRequests);
|
handleSetCookieRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* #open(DataSpec)}.
|
||||||
|
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor)} and {@link
|
||||||
|
* #setContentTypePredicate(Predicate)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public CronetDataSource(
|
||||||
|
CronetEngine cronetEngine,
|
||||||
|
Executor executor,
|
||||||
|
@Nullable Predicate<String> contentTypePredicate) {
|
||||||
|
this(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* #open(DataSpec)}.
|
||||||
|
* @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.
|
||||||
|
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||||
|
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public CronetDataSource(
|
||||||
|
CronetEngine cronetEngine,
|
||||||
|
Executor executor,
|
||||||
|
@Nullable Predicate<String> contentTypePredicate,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
@Nullable RequestProperties defaultRequestProperties) {
|
||||||
|
this(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
contentTypePredicate,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
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 contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||||
|
* predicate then an {@link InvalidContentTypeException} is thrown from {@link
|
||||||
|
* #open(DataSpec)}.
|
||||||
|
* @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.
|
||||||
|
* @deprecated Use {@link #CronetDataSource(CronetEngine, Executor, int, int, boolean,
|
||||||
|
* RequestProperties, boolean)} and {@link #setContentTypePredicate(Predicate)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public CronetDataSource(
|
||||||
|
CronetEngine cronetEngine,
|
||||||
|
Executor executor,
|
||||||
|
@Nullable Predicate<String> contentTypePredicate,
|
||||||
|
int connectTimeoutMs,
|
||||||
|
int readTimeoutMs,
|
||||||
|
boolean resetTimeoutOnRedirects,
|
||||||
|
@Nullable RequestProperties defaultRequestProperties,
|
||||||
|
boolean handleSetCookieRequests) {
|
||||||
|
this(
|
||||||
|
cronetEngine,
|
||||||
|
executor,
|
||||||
|
connectTimeoutMs,
|
||||||
|
readTimeoutMs,
|
||||||
|
resetTimeoutOnRedirects,
|
||||||
|
Clock.DEFAULT,
|
||||||
|
defaultRequestProperties,
|
||||||
|
handleSetCookieRequests);
|
||||||
|
this.contentTypePredicate = contentTypePredicate;
|
||||||
|
}
|
||||||
|
|
||||||
/* package */ CronetDataSource(
|
/* package */ CronetDataSource(
|
||||||
CronetEngine cronetEngine,
|
CronetEngine cronetEngine,
|
||||||
Executor executor,
|
Executor executor,
|
||||||
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;
|
||||||
|
|
@ -266,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
|
||||||
|
|
@ -289,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());
|
||||||
}
|
}
|
||||||
|
|
@ -301,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();
|
||||||
|
|
@ -324,10 +435,15 @@ 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 = new InvalidResponseCodeException(responseCode,
|
InvalidResponseCodeException exception =
|
||||||
responseInfo.getAllHeaders(), currentDataSpec);
|
new InvalidResponseCodeException(
|
||||||
|
responseCode,
|
||||||
|
responseInfo.getHttpStatusText(),
|
||||||
|
responseInfo.getAllHeaders(),
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
@ -335,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,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 {
|
||||||
|
|
@ -358,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;
|
||||||
|
|
@ -377,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 {
|
||||||
|
|
@ -432,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) {
|
||||||
|
|
@ -451,6 +659,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns current {@link UrlRequest}. May be null if the data source is not opened. */
|
||||||
|
@Nullable
|
||||||
|
protected UrlRequest getCurrentUrlRequest() {
|
||||||
|
return currentUrlRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns current {@link UrlResponseInfo}. May be null if the data source is not opened. */
|
||||||
|
@Nullable
|
||||||
|
protected UrlResponseInfo getCurrentUrlResponseInfo() {
|
||||||
|
return responseInfo;
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
|
private UrlRequest.Builder buildRequestBuilder(DataSpec dataSpec) throws IOException {
|
||||||
|
|
@ -476,6 +696,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
|
||||||
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
throw new IOException("HTTP request with non-empty body must set Content-Type");
|
||||||
}
|
}
|
||||||
|
if (dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA)) {
|
||||||
|
requestBuilder.addHeader(
|
||||||
|
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME,
|
||||||
|
IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE);
|
||||||
|
}
|
||||||
// Set the Range header.
|
// Set the Range header.
|
||||||
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
|
||||||
StringBuilder rangeValue = new StringBuilder();
|
StringBuilder rangeValue = new StringBuilder();
|
||||||
|
|
@ -487,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)) {
|
||||||
|
|
@ -516,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");
|
||||||
|
|
@ -594,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
|
||||||
|
|
@ -606,12 +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(responseCode, info.getAllHeaders(), currentDataSpec);
|
new InvalidResponseCodeException(
|
||||||
|
responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
|
||||||
operation.open();
|
operation.open();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -620,13 +902,20 @@ 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();
|
}
|
||||||
|
|
||||||
|
List<String> setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
|
||||||
|
if (isEmpty(setCookieHeaders)) {
|
||||||
|
request.followRedirect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
urlRequest.cancel();
|
||||||
DataSpec redirectUrlDataSpec;
|
DataSpec redirectUrlDataSpec;
|
||||||
if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
|
||||||
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
|
||||||
// transformed into a GET.
|
// transformed into a GET.
|
||||||
redirectUrlDataSpec =
|
redirectUrlDataSpec =
|
||||||
|
|
@ -634,13 +923,13 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
Uri.parse(newLocationUrl),
|
Uri.parse(newLocationUrl),
|
||||||
DataSpec.HTTP_METHOD_GET,
|
DataSpec.HTTP_METHOD_GET,
|
||||||
/* httpBody= */ null,
|
/* httpBody= */ null,
|
||||||
currentDataSpec.absoluteStreamPosition,
|
dataSpec.absoluteStreamPosition,
|
||||||
currentDataSpec.position,
|
dataSpec.position,
|
||||||
currentDataSpec.length,
|
dataSpec.length,
|
||||||
currentDataSpec.key,
|
dataSpec.key,
|
||||||
currentDataSpec.flags);
|
dataSpec.flags);
|
||||||
} else {
|
} else {
|
||||||
redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
|
redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
|
||||||
}
|
}
|
||||||
UrlRequest.Builder requestBuilder;
|
UrlRequest.Builder requestBuilder;
|
||||||
try {
|
try {
|
||||||
|
|
@ -649,12 +938,11 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||||
exception = e;
|
exception = e;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
|
String cookieHeadersValue = parseCookies(setCookieHeaders);
|
||||||
attachCookies(requestBuilder, cookieHeadersValue);
|
attachCookies(requestBuilder, cookieHeadersValue);
|
||||||
currentUrlRequest = requestBuilder.build();
|
currentUrlRequest = requestBuilder.build();
|
||||||
currentUrlRequest.start();
|
currentUrlRequest.start();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
|
public synchronized void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
package com.google.android.exoplayer2.ext.cronet;
|
package com.google.android.exoplayer2.ext.cronet;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import android.util.Log;
|
import androidx.annotation.Nullable;
|
||||||
|
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.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
|
@ -36,13 +38,14 @@ 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
|
||||||
* #SOURCE_UNKNOWN}, {@link #SOURCE_USER_PROVIDED} or {@link #SOURCE_UNAVAILABLE}.
|
* #SOURCE_UNKNOWN}, {@link #SOURCE_USER_PROVIDED} or {@link #SOURCE_UNAVAILABLE}.
|
||||||
*/
|
*/
|
||||||
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
|
@IntDef({SOURCE_NATIVE, SOURCE_GMS, SOURCE_UNKNOWN, SOURCE_USER_PROVIDED, SOURCE_UNAVAILABLE})
|
||||||
public @interface CronetEngineSource {}
|
public @interface CronetEngineSource {}
|
||||||
|
|
@ -142,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,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>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -28,10 +29,9 @@ import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
|
|
||||||
/** Tests for {@link ByteArrayUploadDataProvider}. */
|
/** Tests for {@link ByteArrayUploadDataProvider}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class ByteArrayUploadDataProviderTest {
|
public final class ByteArrayUploadDataProviderTest {
|
||||||
|
|
||||||
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -31,13 +31,13 @@ import static org.mockito.Mockito.when;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ConditionVariable;
|
import android.os.ConditionVariable;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
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;
|
||||||
|
|
@ -62,10 +62,9 @@ import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
|
||||||
|
|
||||||
/** Tests for {@link CronetDataSource}. */
|
/** Tests for {@link CronetDataSource}. */
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class CronetDataSourceTest {
|
public final class CronetDataSourceTest {
|
||||||
|
|
||||||
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
private static final int TEST_CONNECT_TIMEOUT_MS = 100;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
manifest=src/test/AndroidManifest.xml
|
|
||||||
|
|
@ -46,7 +46,7 @@ HOST_PLATFORM="linux-x86_64"
|
||||||
be supported. See the [Supported formats][] page for more details of the
|
be supported. See the [Supported formats][] page for more details of the
|
||||||
available flags.
|
available flags.
|
||||||
|
|
||||||
For example, to fetch and build for armeabi-v7a,
|
For example, to fetch and build FFmpeg release 4.0 for armeabi-v7a,
|
||||||
arm64-v8a and x86 on Linux x86_64:
|
arm64-v8a and x86 on Linux x86_64:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -71,7 +71,7 @@ COMMON_OPTIONS="\
|
||||||
" && \
|
" && \
|
||||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||||
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
|
(git -C ffmpeg pull || git clone git://source.ffmpeg.org/ffmpeg ffmpeg) && \
|
||||||
cd ffmpeg && \
|
cd ffmpeg && git checkout release/4.0 && \
|
||||||
./configure \
|
./configure \
|
||||||
--libdir=android-libs/armeabi-v7a \
|
--libdir=android-libs/armeabi-v7a \
|
||||||
--arch=arm \
|
--arch=arm \
|
||||||
|
|
@ -147,11 +147,11 @@ then implement your own logic to use the renderer for a given track.
|
||||||
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
|
||||||
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
|
||||||
[#2781]: https://github.com/google/ExoPlayer/issues/2781
|
[#2781]: https://github.com/google/ExoPlayer/issues/2781
|
||||||
[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension
|
[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
|
||||||
|
|
||||||
## Links ##
|
## Links ##
|
||||||
|
|
||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
@ -33,12 +32,16 @@ android {
|
||||||
jniLibs.srcDir 'src/main/libs'
|
jniLibs.srcDir 'src/main/libs'
|
||||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -145,12 +145,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isOutputSupported(Format inputFormat) {
|
private boolean isOutputSupported(Format inputFormat) {
|
||||||
return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT);
|
return shouldUseFloatOutput(inputFormat)
|
||||||
|
|| supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_16BIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldUseFloatOutput(Format inputFormat) {
|
private boolean shouldUseFloatOutput(Format inputFormat) {
|
||||||
Assertions.checkNotNull(inputFormat.sampleMimeType);
|
Assertions.checkNotNull(inputFormat.sampleMimeType);
|
||||||
if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) {
|
if (!enableFloatOutput || !supportsOutput(inputFormat.channelCount, C.ENCODING_PCM_FLOAT)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (inputFormat.sampleMimeType) {
|
switch (inputFormat.sampleMimeType) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import android.support.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.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
|
|
@ -37,8 +37,12 @@ import java.util.List;
|
||||||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
|
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
|
||||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||||
|
|
||||||
|
// Error codes matching ffmpeg_jni.cc.
|
||||||
|
private static final int DECODER_ERROR_INVALID_DATA = -1;
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
@ -106,8 +110,14 @@ import java.util.List;
|
||||||
int inputSize = inputData.limit();
|
int inputSize = inputData.limit();
|
||||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
||||||
if (result < 0) {
|
if (result == DECODER_ERROR_INVALID_DATA) {
|
||||||
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
|
// Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will
|
||||||
|
// be produced for this buffer, so mark it as decode-only to ensure that the audio sink's
|
||||||
|
// position is reset when more audio is produced.
|
||||||
|
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
|
||||||
|
return null;
|
||||||
|
} else if (result == DECODER_ERROR_OTHER) {
|
||||||
|
return new FfmpegDecoderException("Error decoding (see logcat).");
|
||||||
}
|
}
|
||||||
if (!hasOutputFormat) {
|
if (!hasOutputFormat) {
|
||||||
channelCount = ffmpegGetChannelCount(nativeContext);
|
channelCount = ffmpegGetChannelCount(nativeContext);
|
||||||
|
|
@ -162,10 +172,35 @@ 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:
|
||||||
|
return getVorbisExtraData(initializationData);
|
||||||
|
default:
|
||||||
|
// Other codecs do not require extra data.
|
||||||
|
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[] header0 = initializationData.get(0);
|
||||||
byte[] header1 = initializationData.get(1);
|
byte[] header1 = initializationData.get(1);
|
||||||
byte[] extraData = new byte[header0.length + header1.length + 6];
|
byte[] extraData = new byte[header0.length + header1.length + 6];
|
||||||
|
|
@ -178,10 +213,6 @@ import java.util.List;
|
||||||
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
|
extraData[header0.length + 5] = (byte) (header1.length & 0xFF);
|
||||||
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
|
System.arraycopy(header1, 0, extraData, header0.length + 6, header1.length);
|
||||||
return extraData;
|
return extraData;
|
||||||
default:
|
|
||||||
// Other codecs do not require extra data.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private native long ffmpegInitialize(
|
private native long ffmpegInitialize(
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,11 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.ffmpeg;
|
package com.google.android.exoplayer2.ext.ffmpeg;
|
||||||
|
|
||||||
import android.support.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.util.LibraryLoader;
|
import com.google.android.exoplayer2.util.LibraryLoader;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,6 +31,8 @@ public final class FfmpegLibrary {
|
||||||
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
|
ExoPlayerLibraryInfo.registerModule("goog.exo.ffmpeg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "FfmpegLibrary";
|
||||||
|
|
||||||
private static final LibraryLoader LOADER =
|
private static final LibraryLoader LOADER =
|
||||||
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
|
new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
|
||||||
|
|
||||||
|
|
@ -69,7 +72,14 @@ public final class FfmpegLibrary {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String codecName = getCodecName(mimeType, encoding);
|
String codecName = getCodecName(mimeType, encoding);
|
||||||
return codecName != null && ffmpegHasDecoder(codecName);
|
if (codecName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ffmpegHasDecoder(codecName)) {
|
||||||
|
Log.w(TAG, "No " + codecName + " decoder available. Check the FFmpeg build configuration.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
|
||||||
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
||||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
||||||
|
|
||||||
|
// Error codes matching FfmpegDecoder.java.
|
||||||
|
static const int DECODER_ERROR_INVALID_DATA = -1;
|
||||||
|
static const int DECODER_ERROR_OTHER = -2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the AVCodec with the specified name, or NULL if it is not available.
|
* Returns the AVCodec with the specified name, or NULL if it is not available.
|
||||||
*/
|
*/
|
||||||
|
|
@ -79,7 +83,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes the packet into the output buffer, returning the number of bytes
|
* Decodes the packet into the output buffer, returning the number of bytes
|
||||||
* written, or a negative value in the case of an error.
|
* written, or a negative DECODER_ERROR constant value in the case of an error.
|
||||||
*/
|
*/
|
||||||
int decodePacket(AVCodecContext *context, AVPacket *packet,
|
int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
uint8_t *outputBuffer, int outputSize);
|
uint8_t *outputBuffer, int outputSize);
|
||||||
|
|
@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
||||||
context->channels = rawChannelCount;
|
context->channels = rawChannelCount;
|
||||||
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
|
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
|
||||||
}
|
}
|
||||||
|
context->err_recognition = AV_EF_IGNORE_ERR;
|
||||||
int result = avcodec_open2(context, codec, NULL);
|
int result = avcodec_open2(context, codec, NULL);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
logError("avcodec_open2", result);
|
logError("avcodec_open2", result);
|
||||||
|
|
@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||||
result = avcodec_send_packet(context, packet);
|
result = avcodec_send_packet(context, packet);
|
||||||
if (result) {
|
if (result) {
|
||||||
logError("avcodec_send_packet", result);
|
logError("avcodec_send_packet", result);
|
||||||
return result;
|
return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA
|
||||||
|
: DECODER_ERROR_OTHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dequeue output data until it runs out.
|
// Dequeue output data until it runs out.
|
||||||
|
|
|
||||||
19
extensions/ffmpeg/src/test/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?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 package="com.google.android.exoplayer2.ext.ffmpeg">
|
||||||
|
<uses-sdk/>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* 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.ffmpeg;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class DefaultRenderersFactoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createRenderers_instantiatesVpxRenderer() {
|
||||||
|
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
|
||||||
|
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,18 +28,19 @@ EXOPLAYER_ROOT="$(pwd)"
|
||||||
FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
|
FLAC_EXT_PATH="${EXOPLAYER_ROOT}/extensions/flac/src/main"
|
||||||
```
|
```
|
||||||
|
|
||||||
* Download the [Android NDK][] and set its location in an environment variable:
|
* Download the [Android NDK][] (version <= 17c) and set its location in an
|
||||||
|
environment variable:
|
||||||
|
|
||||||
```
|
```
|
||||||
NDK_PATH="<path to Android NDK>"
|
NDK_PATH="<path to Android NDK>"
|
||||||
```
|
```
|
||||||
|
|
||||||
* Download and extract flac-1.3.1 as "${FLAC_EXT_PATH}/jni/flac" folder:
|
* Download and extract flac-1.3.2 as "${FLAC_EXT_PATH}/jni/flac" folder:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd "${FLAC_EXT_PATH}/jni" && \
|
cd "${FLAC_EXT_PATH}/jni" && \
|
||||||
curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.1.tar.xz | tar xJ && \
|
curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.2.tar.xz | tar xJ && \
|
||||||
mv flac-1.3.1 flac
|
mv flac-1.3.2 flac
|
||||||
```
|
```
|
||||||
|
|
||||||
* Build the JNI native libraries from the command line:
|
* Build the JNI native libraries from the command line:
|
||||||
|
|
@ -94,4 +95,4 @@ player, then implement your own logic to use the renderer for a given track.
|
||||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
|
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*`
|
||||||
belong to this module.
|
belong to this module.
|
||||||
|
|
||||||
[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html
|
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.ext.compileSdkVersion
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
buildToolsVersion project.ext.buildToolsVersion
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
@ -34,14 +33,18 @@ android {
|
||||||
jniLibs.srcDir 'src/main/libs'
|
jniLibs.srcDir 'src/main/libs'
|
||||||
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions.unitTests.includeAndroidResources = true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
androidTestImplementation 'androidx.test:runner:' + testRunnerVersion
|
implementation 'androidx.annotation:annotation:1.1.0'
|
||||||
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
androidTestImplementation project(modulePrefix + 'testutils')
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation project(modulePrefix + 'testutils-robolectric')
|
androidTestImplementation 'androidx.test:runner:' + androidXTestVersion
|
||||||
|
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 {
|
||||||
*;
|
*;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.google.android.exoplayer2.ext.flac.test">
|
package="com.google.android.exoplayer2.ext.flac.test">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-sdk/>
|
||||||
|
|
||||||
<application android:debuggable="true"
|
<application android:debuggable="true"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,26 @@
|
||||||
package com.google.android.exoplayer2.ext.flac;
|
package com.google.android.exoplayer2.ext.flac;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Unit test for {@link FlacBinarySearchSeeker}. */
|
/** Unit test for {@link FlacBinarySearchSeeker}. */
|
||||||
public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class FlacBinarySearchSeekerTest {
|
||||||
|
|
||||||
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
|
private static final String NOSEEKTABLE_FLAC = "bear_no_seek.flac";
|
||||||
private static final int DURATION_US = 2_741_000;
|
private static final int DURATION_US = 2_741_000;
|
||||||
|
|
||||||
@Override
|
@Before
|
||||||
protected void setUp() throws Exception {
|
public void setUp() {
|
||||||
super.setUp();
|
|
||||||
if (!FlacLibrary.isAvailable()) {
|
if (!FlacLibrary.isAvailable()) {
|
||||||
fail("Flac library not available.");
|
fail("Flac library not available.");
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +43,8 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
|
public void testGetSeekMap_returnsSeekMapWithCorrectDuration()
|
||||||
throws IOException, FlacDecoderException, InterruptedException {
|
throws IOException, FlacDecoderException, InterruptedException {
|
||||||
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
|
byte[] data =
|
||||||
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
|
||||||
|
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||||
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
||||||
|
|
@ -47,7 +52,10 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
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();
|
||||||
|
|
@ -57,14 +65,18 @@ public final class FlacBinarySearchSeekerTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
public void testSetSeekTargetUs_returnsSeekPending()
|
public void testSetSeekTargetUs_returnsSeekPending()
|
||||||
throws IOException, FlacDecoderException, InterruptedException {
|
throws IOException, FlacDecoderException, InterruptedException {
|
||||||
byte[] data = TestUtil.getByteArray(getInstrumentation().getContext(), NOSEEKTABLE_FLAC);
|
byte[] data =
|
||||||
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NOSEEKTABLE_FLAC);
|
||||||
|
|
||||||
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
|
||||||
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
FlacDecoderJni decoderJni = new FlacDecoderJni();
|
||||||
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();
|
||||||
|
|
|
||||||