Merge pull request #5 from google/dev-v2

Merge from google/ExoPlayer/dev-v2
This commit is contained in:
BAI Yanning 2019-12-23 09:17:18 +08:00 committed by GitHub
commit c17d1467f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1385 changed files with 80753 additions and 33049 deletions

57
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@ -0,0 +1,57 @@
---
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 youre able to share as source code on GitHub.
### [REQUIRED] Link to test content
Provide a JSON snippet for the demo apps 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.

View File

@ -0,0 +1,30 @@
---
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.

50
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,50 @@
---
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. Its 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 youve already looked for an answer to your question. Its
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 apps 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.

7
.gitignore vendored
View File

@ -57,6 +57,9 @@ extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
@ -71,7 +74,3 @@ extensions/cronet/jniLibs/*
!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

View File

@ -12,13 +12,14 @@ libs
obj obj
lint.xml lint.xml
# IntelliJ IDEA # IntelliJ IDEA & Android Studio
.idea .idea
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
classes classes
gen-external-apklibs gen-external-apklibs
*.li
# Eclipse # Eclipse
.project .project
@ -61,6 +62,9 @@ extensions/vp9/src/main/jni/libvpx
extensions/vp9/src/main/jni/libvpx_android_configs extensions/vp9/src/main/jni/libvpx_android_configs
extensions/vp9/src/main/jni/libyuv extensions/vp9/src/main/jni/libyuv
# AV1 extension
extensions/av1/src/main/jni/libgav1
# Opus extension # Opus extension
extensions/opus/src/main/jni/libopus extensions/opus/src/main/jni/libopus
@ -75,7 +79,3 @@ extensions/cronet/jniLibs/*
!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

View File

@ -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".

View File

@ -15,8 +15,8 @@ and extend, and can be updated through Play Store application updates.
* Follow our [developer blog][] to keep up to date with the latest ExoPlayer * Follow our [developer blog][] to keep up to date with the latest ExoPlayer
developments! developments!
[developer guide]: https://google.github.io/ExoPlayer/guide.html [developer guide]: https://exoplayer.dev/guide.html
[class reference]: https://google.github.io/ExoPlayer/doc/reference [class reference]: https://exoplayer.dev/doc/reference
[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md [release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md
[developer blog]: https://medium.com/google-exoplayer [developer blog]: https://medium.com/google-exoplayer
@ -95,20 +95,6 @@ compileOptions {
} }
``` ```
Note that if you want to use Java 8 features in your own code, the following
additional options need to be set:
```gradle
// For Java compilers:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin compilers:
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
```
### Locally ### ### Locally ###
Cloning the repository and depending on the modules locally is required when Cloning the repository and depending on the modules locally is required when
@ -121,6 +107,7 @@ branch:
```sh ```sh
git clone https://github.com/google/ExoPlayer.git git clone https://github.com/google/ExoPlayer.git
cd ExoPlayer
git checkout release-v2 git checkout release-v2
``` ```

View File

@ -2,20 +2,465 @@
### dev-v2 (not yet released) ### ### dev-v2 (not yet released) ###
* Support for playing spherical videos on Daydream. * Add Java FLAC extractor
* Improve decoder re-use between playbacks. TODO: Write and link a blog post ([#6406](https://github.com/google/ExoPlayer/issues/6406)).
here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). If `DefaultExtractorsFactory` is used, this extractor is only used if the FLAC
extension is not loaded.
* Make `MediaSourceEventListener.LoadEventInfo` and
`MediaSourceEventListener.MediaLoadData` top-level classes.
* Rename `MediaCodecRenderer.onOutputFormatChanged` to
`MediaCodecRenderer.onOutputMediaFormatChanged`, further
clarifying the distinction between `Format` and `MediaFormat`.
* Downloads: Merge downloads in `SegmentDownloader` to improve overall download
speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
* In MP4 streams, store the Android capture frame rate only in
`Format.metadata`. `Format.frameRate` now stores the calculated frame rate.
* Add `play` and `pause` methods to `Player`.
* Upgrade Truth dependency from 0.44 to 1.0.
* Upgrade to JUnit 4.13-rc-2.
* Add support for attaching DRM sessions to clear content in the demo app.
* Add `SpannedSubject` to testutils, for assertions on
[Span-styled text]( https://developer.android.com/guide/topics/text/spans)
(e.g. subtitles).
* Add `Player.getCurrentLiveOffset` to conveniently return the live offset.
* Update `IcyDecoder` to try ISO-8859-1 decoding if UTF-8 decoding fails.
Also change `IcyInfo.rawMetadata` from `String` to `byte[]` to allow
developers to handle data that's neither UTF-8 nor ISO-8859-1
([#6753](https://github.com/google/ExoPlayer/issues/6753)).
* Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)).
* Fix handling of network transitions in `RequirementsWatcher`
([#6733](https://github.com/google/ExoPlayer/issues/6733)). Incorrect handling
could previously cause downloads to be paused when they should have been able
to proceed.
### 2.11.1 (2019-12-20) ###
* UI: Exclude `DefaultTimeBar` region from system gesture detection
([#6685](https://github.com/google/ExoPlayer/issues/6685)).
* ProGuard fixes:
* Ensure `Libgav1VideoRenderer` constructor is kept for use by
`DefaultRenderersFactory`
([#6773](https://github.com/google/ExoPlayer/issues/6773)).
* Ensure `VideoDecoderOutputBuffer` and its members are kept for use by video
decoder extensions.
* Ensure raw resources used with `RawResourceDataSource` are kept.
* Suppress spurious warnings about the `javax.annotation` package, and
restructure use of `IntDef` annotations to remove spurious warnings about
`SsaStyle$SsaAlignment`
([#6771](https://github.com/google/ExoPlayer/issues/6771)).
* Fix `CacheDataSource` to correctly propagate `DataSpec.httpRequestHeaders`.
* Fix issue with `DefaultDownloadIndex` that could result in an
`IllegalStateException` being thrown from
`DefaultDownloadIndex.getDownloadForCurrentRow`
([#6785](https://github.com/google/ExoPlayer/issues/6785)).
* Fix `IndexOutOfBoundsException` in `SinglePeriodTimeline.getWindow`
([#6776](https://github.com/google/ExoPlayer/issues/6776)).
* Add missing `@Nullable` to `MediaCodecAudioRenderer.getMediaClock` and
`SimpleDecoderAudioRenderer.getMediaClock`
([#6792](https://github.com/google/ExoPlayer/issues/6792)).
### 2.11.0 (2019-12-11) ###
* Core library:
* Replace `ExoPlayerFactory` by `SimpleExoPlayer.Builder` and
`ExoPlayer.Builder`.
* Add automatic `WakeLock` handling to `SimpleExoPlayer`, which can be enabled
by calling `SimpleExoPlayer.setHandleWakeLock`
([#5846](https://github.com/google/ExoPlayer/issues/5846)). To use this
feature, you must add the
[WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK)
permission to your application's manifest file.
* Add automatic "audio becoming noisy" handling to `SimpleExoPlayer`, which
can be enabled by calling `SimpleExoPlayer.setHandleAudioBecomingNoisy`.
* Wrap decoder exceptions in a new `DecoderException` class and report them as
renderer errors.
* Add `Timeline.Window.isLive` to indicate that a window is a live stream
([#2668](https://github.com/google/ExoPlayer/issues/2668) and
[#5973](https://github.com/google/ExoPlayer/issues/5973)).
* Add `Timeline.Window.uid` to uniquely identify window instances.
* Deprecate `setTag` parameter of `Timeline.getWindow`. Tags will always be
set.
* Deprecate passing the manifest directly to
`Player.EventListener.onTimelineChanged`. It can be accessed through
`Timeline.Window.manifest` or `Player.getCurrentManifest()`
* Add `MediaSource.enable` and `MediaSource.disable` to improve resource
management in playlists.
* Add `MediaPeriod.isLoading` to improve `Player.isLoading` state.
* Fix issue where player errors are thrown too early at playlist transitions
([#5407](https://github.com/google/ExoPlayer/issues/5407)).
* Add `Format` and renderer support flags to renderer `ExoPlaybackException`s.
* Where there are multiple platform decoders for a given MIME type, prefer to
use one that advertises support for the profile and level of the media being
played over one that does not, even if it does not come first in the
`MediaCodecList`.
* DRM:
* Inject `DrmSessionManager` into the `MediaSources` instead of `Renderers`.
This allows each `MediaSource` in a `ConcatenatingMediaSource` to use a
different `DrmSessionManager`
([#5619](https://github.com/google/ExoPlayer/issues/5619)).
* Add `DefaultDrmSessionManager.Builder`, and remove
`DefaultDrmSessionManager` static factory methods that leaked
`ExoMediaDrm` instances
([#4721](https://github.com/google/ExoPlayer/issues/4721)).
* Add support for the use of secure decoders when playing clear content
([#4867](https://github.com/google/ExoPlayer/issues/4867)). This can
be enabled using `DefaultDrmSessionManager.Builder`'s
`setUseDrmSessionsForClearContent` method.
* Add support for custom `LoadErrorHandlingPolicies` in key and provisioning
requests ([#6334](https://github.com/google/ExoPlayer/issues/6334)). Custom
policies can be passed via `DefaultDrmSessionManager.Builder`'s
`setLoadErrorHandlingPolicy` method.
* Use `ExoMediaDrm.Provider` in `OfflineLicenseHelper` to avoid leaking
`ExoMediaDrm` instances
([#4721](https://github.com/google/ExoPlayer/issues/4721)).
* Track selection: * Track selection:
* Add options for controlling audio track selections to `DefaultTrackSelector` * Update `DefaultTrackSelector` to set a viewport constraint for the default
([#3314](https://github.com/google/ExoPlayer/issues/3314)). display by default.
* Update `TrackSelection.Factory` interface to support creating all track * Update `DefaultTrackSelector` to set text language and role flag
selections together. constraints for the device's accessibility settings by default
* Do not retry failed loads whose error is `FileNotFoundException`. ([#5749](https://github.com/google/ExoPlayer/issues/5749)).
* Add option to set preferred text role flags using
`DefaultTrackSelector.ParametersBuilder.setPreferredTextRoleFlags`.
* Android 10:
* Set `compileSdkVersion` to 29 to enable use of Android 10 APIs.
* Expose new `isHardwareAccelerated`, `isSoftwareOnly` and `isVendor` flags
in `MediaCodecInfo`
([#5839](https://github.com/google/ExoPlayer/issues/5839)).
* Add `allowedCapturePolicy` field to `AudioAttributes` to allow to
configuration of the audio capture policy.
* Video:
* Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`.
* Fix byte order of HDR10+ static metadata to match CTA-861.3.
* Support out-of-band HDR10+ dynamic metadata for VP9 in WebM/Matroska.
* Assume that protected content requires a secure decoder when evaluating
whether `MediaCodecVideoRenderer` supports a given video format
([#5568](https://github.com/google/ExoPlayer/issues/5568)).
* Fix Dolby Vision fallback to AVC and HEVC.
* Fix early end-of-stream detection when using video tunneling, on API level
23 and above.
* Fix an issue where a keyframe was rendered rather than skipped when
performing an exact seek to a non-zero position close to the start of the
stream.
* Audio:
* Fix the start of audio getting truncated when transitioning to a new
item in a playlist of Opus streams.
* Workaround broken raw audio decoding on Oppo R9
([#5782](https://github.com/google/ExoPlayer/issues/5782)).
* Reconfigure audio sink when PCM encoding changes
([#6601](https://github.com/google/ExoPlayer/issues/6601)).
* Allow `AdtsExtractor` to encounter EOF when calculating average frame size
([#6700](https://github.com/google/ExoPlayer/issues/6700)).
* Text:
* Add support for position and overlapping start/end times in SSA/ASS
subtitles ([#6320](https://github.com/google/ExoPlayer/issues/6320)).
* Require an end time or duration for SubRip (SRT) and SubStation Alpha
(SSA/ASS) subtitles. This applies to both sidecar files & subtitles
[embedded in Matroska streams](https://matroska.org/technical/specs/subtitles/index.html).
* UI:
* Make showing and hiding player controls accessible to TalkBack in
`PlayerView`.
* Rename `spherical_view` surface type to `spherical_gl_surface_view`.
* Make it easier to override the shuffle, repeat, fullscreen, VR and small
notification icon assets
([#6709](https://github.com/google/ExoPlayer/issues/6709)).
* Analytics:
* Remove `AnalyticsCollector.Factory`. Instances should be created directly,
and the `Player` should be set by calling `AnalyticsCollector.setPlayer`.
* Add `PlaybackStatsListener` to collect `PlaybackStats` for analysis and
analytics reporting.
* DataSource
* Add `DataSpec.httpRequestHeaders` to support setting per-request headers for
HTTP and HTTPS.
* Remove the `DataSpec.FLAG_ALLOW_ICY_METADATA` flag. Use is replaced by
setting the `IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME` header in
`DataSpec.httpRequestHeaders`.
* Fail more explicitly when local file URIs contain invalid parts (e.g. a
fragment) ([#6470](https://github.com/google/ExoPlayer/issues/6470)).
* DASH: Support negative @r values in segment timelines
([#1787](https://github.com/google/ExoPlayer/issues/1787)).
* HLS:
* Use peak bitrate rather than average bitrate for adaptive track selection.
* Fix issue where streams could get stuck in an infinite buffering state
after a postroll ad
([#6314](https://github.com/google/ExoPlayer/issues/6314)).
* Matroska: Support lacing in Blocks
([#3026](https://github.com/google/ExoPlayer/issues/3026)).
* AV1 extension:
* New in this release. The AV1 extension allows use of the
[libgav1 software decoder](https://chromium.googlesource.com/codecs/libgav1/)
in ExoPlayer. You can read more about playing AV1 videos with ExoPlayer
[here](https://medium.com/google-exoplayer/playing-av1-videos-with-exoplayer-a7cb19bedef9).
* VP9 extension:
* Update to use NDK r20.
* Rename `VpxVideoSurfaceView` to `VideoDecoderSurfaceView` and move it to the
core library.
* Move `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` to
`C.MSG_SET_OUTPUT_BUFFER_RENDERER`.
* Use `VideoDecoderRenderer` as an implementation of
`VideoDecoderOutputBufferRenderer`, instead of `VideoDecoderSurfaceView`.
* Flac extension: Update to use NDK r20.
* Opus extension: Update to use NDK r20.
* FFmpeg extension:
* Update to use NDK r20.
* Update to use FFmpeg version 4.2. It is necessary to rebuild the native part
of the extension after this change, following the instructions in the
extension's readme.
* MediaSession extension: Add `MediaSessionConnector.setCaptionCallback` to
support `ACTION_SET_CAPTIONING_ENABLED` events.
* GVR extension: This extension is now deprecated.
* Demo apps:
* Add [SurfaceControl demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/surface)
to show how to use the Android 10 `SurfaceControl` API with ExoPlayer
([#677](https://github.com/google/ExoPlayer/issues/677)).
* Add support for subtitle files to the
[Main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main)
([#5523](https://github.com/google/ExoPlayer/issues/5523)).
* Remove the IMA demo app. IMA functionality is demonstrated by the
[main demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/main).
* Add basic DRM support to the
[Cast demo app](https://github.com/google/ExoPlayer/tree/r2.11.0/demos/cast).
* TestUtils: Publish the `testutils` module to simplify unit testing with
ExoPlayer ([#6267](https://github.com/google/ExoPlayer/issues/6267)).
* IMA extension: Remove `AdsManager` listeners on release to avoid leaking an
`AdEventListener` provided by the app
([#6687](https://github.com/google/ExoPlayer/issues/6687)).
### 2.10.8 (2019-11-19) ###
* E-AC3 JOC
* Handle new signaling in DASH manifests
([#6636](https://github.com/google/ExoPlayer/issues/6636)).
* Fix E-AC3 JOC passthrough playback failing to initialize due to incorrect
channel count check.
* FLAC
* Fix sniffing for some FLAC streams.
* Fix FLAC `Format.bitrate` values.
* Parse ALAC channel count and sample rate information from a more robust source
when contained in MP4
([#6648](https://github.com/google/ExoPlayer/issues/6648)).
* Fix seeking into multi-period content in the edge case that the period
containing the seek position has just been removed
([#6641](https://github.com/google/ExoPlayer/issues/6641)).
### 2.10.7 (2019-11-06) ###
* HLS: Fix detection of Dolby Atmos to match the HLS authoring specification.
* MediaSession extension: Update shuffle and repeat modes when playback state
is invalidated ([#6582](https://github.com/google/ExoPlayer/issues/6582)).
* Fix the start of audio getting truncated when transitioning to a new
item in a playlist of Opus streams.
### 2.10.6 (2019-10-17) ###
* Add `Player.onPlaybackSuppressionReasonChanged` to allow listeners to
detect playbacks suppressions (e.g. transient audio focus loss) directly
([#6203](https://github.com/google/ExoPlayer/issues/6203)).
* DASH:
* Support `Label` elements
([#6297](https://github.com/google/ExoPlayer/issues/6297)).
* Support legacy audio channel configuration
([#6523](https://github.com/google/ExoPlayer/issues/6523)).
* HLS: Add support for ID3 in EMSG when using FMP4 streams
([spec](https://aomediacodec.github.io/av1-id3/)).
* MP3: Add workaround to avoid prematurely ending playback of some SHOUTcast
live streams ([#6537](https://github.com/google/ExoPlayer/issues/6537),
[#6315](https://github.com/google/ExoPlayer/issues/6315) and
[#5658](https://github.com/google/ExoPlayer/issues/5658)).
* Metadata: Expose the raw ICY metadata through `IcyInfo`
([#6476](https://github.com/google/ExoPlayer/issues/6476)).
* UI:
* Setting `app:played_color` on `PlayerView` and `PlayerControlView` no longer
adjusts the colors of the scrubber handle , buffered and unplayed parts of
the time bar. These can be set separately using `app:scrubber_color`,
`app:buffered_color` and `app_unplayed_color` respectively.
* Setting `app:ad_marker_color` on `PlayerView` and `PlayerControlView` no
longer adjusts the color of played ad markers. The color of played ad
markers can be set separately using `app:played_ad_marker_color`.
### 2.10.5 (2019-09-20) ###
* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether
the playback position is advancing. This helps to determine if playback is
suppressed due to audio focus loss. Also add
`Player.getPlaybackSuppressedReason` to determine the reason of the
suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)).
* Track selection
* Add `allowAudioMixedChannelCountAdaptiveness` parameter to
`DefaultTrackSelector` to allow adaptive selections of audio tracks with
different channel counts.
* Improve text selection logic to always prefer the better language matches
over other selection parameters.
* Fix audio selection issue where languages are compared by bitrate
([#6335](https://github.com/google/ExoPlayer/issues/6335)).
* Performance
* Increase maximum video buffer size from 13MB to 32MB. The previous default
was too small for high quality streams.
* Reset `DefaultBandwidthMeter` to initial values on network change.
* Bypass sniffing in `ProgressiveMediaPeriod` in case a single extractor is
provided ([#6325](https://github.com/google/ExoPlayer/issues/6325)).
* Metadata
* Support EMSG V1 boxes in FMP4.
* Support unwrapping of nested metadata (e.g. ID3 and SCTE-35 in EMSG).
* Add `HttpDataSource.getResponseCode` to provide the status code associated
with the most recent HTTP response.
* Fix transitions between packed audio and non-packed audio segments in HLS
([#6444](https://github.com/google/ExoPlayer/issues/6444)).
* Fix issue where a request would be retried after encountering an error, even
though the `LoadErrorHandlingPolicy` classified the error as fatal.
* Fix initialization data handling for FLAC in MP4
([#6396](https://github.com/google/ExoPlayer/issues/6396),
[#6397](https://github.com/google/ExoPlayer/issues/6397)).
* Fix decoder selection for E-AC3 JOC streams
([#6398](https://github.com/google/ExoPlayer/issues/6398)).
* Fix `PlayerNotificationManager` to show play icon rather than pause icon when
playback is ended ([#6324](https://github.com/google/ExoPlayer/issues/6324)).
* RTMP extension: Upgrade LibRtmp-Client-for-Android to fix RTMP playback issues
([#4200](https://github.com/google/ExoPlayer/issues/4200),
[#4249](https://github.com/google/ExoPlayer/issues/4249),
[#4319](https://github.com/google/ExoPlayer/issues/4319),
[#4337](https://github.com/google/ExoPlayer/issues/4337)).
* IMA extension: Fix crash in `ImaAdsLoader.onTimelineChanged`
([#5831](https://github.com/google/ExoPlayer/issues/5831)).
### 2.10.4 (2019-07-26) ###
* 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 (2019-07-09) ###
* 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 (2019-06-03) ###
* 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: * Offline:
* Speed up removal of segmented downloads * Prevent unexpected `DownloadHelper.Callback.onPrepared` callbacks after
([#5136](https://github.com/google/ExoPlayer/issues/5136)). preparation of a `DownloadHelper` fails
* Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS ([#5915](https://github.com/google/ExoPlayer/issues/5915)).
media sources to simplify filtering by downloaded streams. * 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 (2019-05-16) ###
* 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 (2019-04-15) ###
* 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.
* Make the default minimum buffer size equal the maximum buffer size for video
playbacks ([#2083](https://github.com/google/ExoPlayer/issues/2083)).
* 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: * Caching:
* Improve performance of `SimpleCache` * Improve performance of `SimpleCache`
([#4253](https://github.com/google/ExoPlayer/issues/4253)). ([#4253](https://github.com/google/ExoPlayer/issues/4253)).
@ -23,31 +468,138 @@
this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been
replaced with an opt out flag replaced with an opt out flag
(`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`).
* DownloadManager: * Extractors:
* Create only one task for all DownloadActions for the same content. * MP4/FMP4: Add support for Dolby Vision.
* Rename TaskState to DownloadState. * MP4: Fix issue handling meta atoms in some streams
* Add new states to DownloadState. ([#5698](https://github.com/google/ExoPlayer/issues/5698),
* Replace DownloadState.action with DownloadAction fields. [#5694](https://github.com/google/ExoPlayer/issues/5694)).
* DRM: Fix black flicker when keys rotate in DRM protected content * MP3: Add support for SHOUTcast ICY metadata
([#3561](https://github.com/google/ExoPlayer/issues/3561)). ([#3735](https://github.com/google/ExoPlayer/issues/3735)).
* Add support for SHOUTcast ICY metadata * MP3: Fix ID3 frame unsychronization
([#3735](https://github.com/google/ExoPlayer/issues/3735)). ([#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`.
* Select audio track based on system language if no preference is provided.
* 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 * CEA-608: Improved conformance to the specification
([#3860](https://github.com/google/ExoPlayer/issues/3860)). ([#3860](https://github.com/google/ExoPlayer/issues/3860)).
* IMA extension: Require setting the `Player` on `AdsLoader` instances before * DASH:
playback. * Parse role and accessibility descriptors into `Format.roleFlags`.
* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a * Support multiple CEA-608 channels muxed into FMP4 representations
callback `Runnable`. ([#5656](https://github.com/google/ExoPlayer/issues/5656)).
* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * HLS:
* Change signature of `PlayerNotificationManager.NotificationListener` to better * Prevent unnecessary reloads of initialization segments.
fit service requirements. Remove ability to set a custom stop action. * Form an adaptive track group out of audio renditions with matching name.
* Add workaround for video quality problems with Amlogic decoders * Support encrypted initialization segments
([#5003](https://github.com/google/ExoPlayer/issues/5003)). ([#5441](https://github.com/google/ExoPlayer/issues/5441)).
* Associate fatal player errors of type SOURCE with the loading source in * Parse `EXT-X-MEDIA` `CHARACTERISTICS` attribute into `Format.roleFlags`.
`AnalyticsListener.EventTime` * Add metadata entry for HLS tracks to expose master playlist information.
([#5407](https://github.com/google/ExoPlayer/issues/5407)). * 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.
* 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.4 ### ### 2.9.6 (2019-02-19) ###
* 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 (2019-01-31) ###
* 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 (2019-01-15) ###
* IMA extension: Clear ads loader listeners on release * IMA extension: Clear ads loader listeners on release
([#4114](https://github.com/google/ExoPlayer/issues/4114)). ([#4114](https://github.com/google/ExoPlayer/issues/4114)).
@ -56,6 +608,17 @@
* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior * FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
* GVR extension: upgrade GVR SDK dependency to 1.190.0. * 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 * Fix issue where sending callbacks for playlist changes may cause problems
because of parallel player access because of parallel player access
([#5240](https://github.com/google/ExoPlayer/issues/5240)). ([#5240](https://github.com/google/ExoPlayer/issues/5240)).
@ -64,14 +627,8 @@
([#5351](https://github.com/google/ExoPlayer/issues/5351)). ([#5351](https://github.com/google/ExoPlayer/issues/5351)).
* Fix issue where uneven track durations in MP4 streams can cause OOM problems * Fix issue where uneven track durations in MP4 streams can cause OOM problems
([#3670](https://github.com/google/ExoPlayer/issues/3670)). ([#3670](https://github.com/google/ExoPlayer/issues/3670)).
* 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`.
### 2.9.3 ### ### 2.9.3 (2018-12-20) ###
* Captions: Support PNG subtitles in SMPTE-TT * Captions: Support PNG subtitles in SMPTE-TT
([#1583](https://github.com/google/ExoPlayer/issues/1583)). ([#1583](https://github.com/google/ExoPlayer/issues/1583)).
@ -90,7 +647,7 @@
* Workaround for MiTV (dangal) issue when swapping output surface * Workaround for MiTV (dangal) issue when swapping output surface
([#5169](https://github.com/google/ExoPlayer/issues/5169)). ([#5169](https://github.com/google/ExoPlayer/issues/5169)).
### 2.9.2 ### ### 2.9.2 (2018-11-28) ###
* HLS: * HLS:
* Fix issue causing unnecessary media playlist requests when playing live * Fix issue causing unnecessary media playlist requests when playing live
@ -117,7 +674,7 @@
([#5162](https://github.com/google/ExoPlayer/issues/5162)). ([#5162](https://github.com/google/ExoPlayer/issues/5162)).
* Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27. * Fix UUID passed to `MediaCrypto` when using `C.CLEARKEY_UUID` before API 27.
### 2.9.1 ### ### 2.9.1 (2018-11-01) ###
* Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext` * Add convenience methods `Player.next`, `Player.previous`, `Player.hasNext`
and `Player.hasPrevious` and `Player.hasPrevious`
@ -169,7 +726,7 @@
* Swap recommended order for google() and jcenter() in gradle config * Swap recommended order for google() and jcenter() in gradle config
([#4997](https://github.com/google/ExoPlayer/issues/4997)). ([#4997](https://github.com/google/ExoPlayer/issues/4997)).
### 2.9.0 ### ### 2.9.0 (2018-09-06) ###
* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to * Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to
add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their
@ -261,7 +818,7 @@
and `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: * UI:
* Add option to `PlayerView` to show buffering view when playWhenReady is * Add option to `PlayerView` to show buffering view when playWhenReady is
false ([#4304](https://github.com/google/ExoPlayer/issues/4304)). false ([#4304](https://github.com/google/ExoPlayer/issues/4304)).
* Allow any `Drawable` to be used as `PlayerView` default artwork. * Allow any `Drawable` to be used as `PlayerView` default artwork.
@ -331,13 +888,13 @@
* Cronet extension: Now distributed via jCenter. * Cronet extension: Now distributed via jCenter.
* FFmpeg extension: Support mu-law and A-law PCM. * FFmpeg extension: Support mu-law and A-law PCM.
### 2.8.4 ### ### 2.8.4 (2018-08-17) ###
* IMA extension: 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 (2018-07-23) ###
* IMA extension: * IMA extension:
* Fix behavior when creating/releasing the player then releasing * Fix behavior when creating/releasing the player then releasing
@ -380,7 +937,7 @@
([#4611](https://github.com/google/ExoPlayer/issues/4611)). ([#4611](https://github.com/google/ExoPlayer/issues/4611)).
* Improved compatibility with FireOS devices. * Improved compatibility with FireOS devices.
### 2.8.2 ### ### 2.8.2 (2018-06-06) ###
* IMA extension: Don't advertise support for video/mpeg ad media, as we don't * IMA extension: Don't advertise support for video/mpeg ad media, as we don't
have an extractor for this have an extractor for this
@ -407,7 +964,7 @@
* Allow apps to register custom MIME types * Allow apps to register custom MIME types
([#4264](https://github.com/google/ExoPlayer/issues/4264)). ([#4264](https://github.com/google/ExoPlayer/issues/4264)).
### 2.8.1 ### ### 2.8.1 (2018-05-22) ###
* HLS: * HLS:
* Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags * Fix playback of livestreams with EXT-X-PROGRAM-DATE-TIME tags
@ -417,7 +974,7 @@
* OkHttp extension: Fix to correctly include response headers in thrown * OkHttp extension: Fix to correctly include response headers in thrown
`InvalidResponseCodeException`s. `InvalidResponseCodeException`s.
* Add possibility to cancel `PlayerMessage`s. * Add possibility to cancel `PlayerMessage`s.
* UI components: * UI:
* Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed * Add `PlayerView.setKeepContentOnPlayerReset` to keep the currently displayed
video frame or media artwork visible when the player is reset video frame or media artwork visible when the player is reset
([#2843](https://github.com/google/ExoPlayer/issues/2843)). ([#2843](https://github.com/google/ExoPlayer/issues/2843)).
@ -440,7 +997,7 @@
* Support TTML font size using % correctly (as percentage of document cell * Support TTML font size using % correctly (as percentage of document cell
resolution). resolution).
### 2.8.0 ### ### 2.8.0 (2018-05-03) ###
* Downloading: * Downloading:
* Add `DownloadService`, `DownloadManager` and related classes * Add `DownloadService`, `DownloadManager` and related classes
@ -467,7 +1024,7 @@
* Support live stream clipping with `ClippingMediaSource`. * Support live stream clipping with `ClippingMediaSource`.
* Allow setting tags for all media sources in their factories. The tag of the * Allow setting tags for all media sources in their factories. The tag of the
current window can be retrieved with `Player.getCurrentTag`. current window can be retrieved with `Player.getCurrentTag`.
* UI components: * UI:
* Add support for displaying error messages and a buffering spinner in * Add support for displaying error messages and a buffering spinner in
`PlayerView`. `PlayerView`.
* Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update
@ -554,12 +1111,12 @@
`BaseRenderer.onStreamChanged`. `BaseRenderer.onStreamChanged`.
* Added dependencies on checkerframework annotations for static code analysis. * Added dependencies on checkerframework annotations for static code analysis.
### 2.7.3 ### ### 2.7.3 (2018-04-04) ###
* Fix ProGuard configuration for Cast, IMA and OkHttp extensions. * Fix ProGuard configuration for Cast, IMA and OkHttp extensions.
* Update OkHttp extension to depend on OkHttp 3.10.0. * Update OkHttp extension to depend on OkHttp 3.10.0.
### 2.7.2 ### ### 2.7.2 (2018-03-29) ###
* Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android * Gradle: Upgrade Gradle version from 4.1 to 4.4 so it can work with Android
Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)). Studio 3.1 ([#3708](https://github.com/google/ExoPlayer/issues/3708)).
@ -573,7 +1130,7 @@
* MediaSession extension: Omit fast forward and rewind actions when media is not * MediaSession extension: Omit fast forward and rewind actions when media is not
seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)). seekable ([#4001](https://github.com/google/ExoPlayer/issues/4001)).
### 2.7.1 ### ### 2.7.1 (2018-03-09) ###
* Gradle: Replaced 'compile' (deprecated) with 'implementation' and * Gradle: Replaced 'compile' (deprecated) with 'implementation' and
'api'. This may lead to build breakage for applications upgrading from 'api'. This may lead to build breakage for applications upgrading from
@ -593,7 +1150,7 @@
* Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and * Video: Force rendering a frame periodically in `MediaCodecVideoRenderer` and
`LibvpxVideoRenderer`, even if it is late. `LibvpxVideoRenderer`, even if it is late.
### 2.7.0 ### ### 2.7.0 (2018-02-19) ###
* Player interface: * Player interface:
* Add optional parameter to `stop` to reset the player when stopping. * Add optional parameter to `stop` to reset the player when stopping.
@ -631,7 +1188,7 @@
`SsMediaSource.Factory`, and `MergingMediaSource`. `SsMediaSource.Factory`, and `MergingMediaSource`.
* Play out existing buffer before retrying for progressive live streams * Play out existing buffer before retrying for progressive live streams
([#1606](https://github.com/google/ExoPlayer/issues/1606)). ([#1606](https://github.com/google/ExoPlayer/issues/1606)).
* UI components: * UI:
* Generalized player and control views to allow them to bind with any * Generalized player and control views to allow them to bind with any
`Player`, and renamed them to `PlayerView` and `PlayerControlView` `Player`, and renamed them to `PlayerView` and `PlayerControlView`
respectively. respectively.
@ -719,7 +1276,7 @@
([#3340](https://github.com/google/ExoPlayer/issues/3340)). ([#3340](https://github.com/google/ExoPlayer/issues/3340)).
* Demo app: Add ability to download not DRM protected content. * Demo app: Add ability to download not DRM protected content.
### 2.6.1 ### ### 2.6.1 (2017-12-15) ###
* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, * Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`,
`DashMediaSource` and `SingleSampleMediaSource`. `DashMediaSource` and `SingleSampleMediaSource`.
@ -763,7 +1320,7 @@
* Prevent period transitions when seeking to the end of a period when paused * Prevent period transitions when seeking to the end of a period when paused
([#2439](https://github.com/google/ExoPlayer/issues/2439)). ([#2439](https://github.com/google/ExoPlayer/issues/2439)).
### 2.6.0 ### ### 2.6.0 (2017-11-03) ###
* Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". * Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0".
* New `Player.DefaultEventListener` abstract class can be extended to avoid * New `Player.DefaultEventListener` abstract class can be extended to avoid
@ -833,7 +1390,7 @@
* Unit tests moved to Robolectric. * Unit tests moved to Robolectric.
* Misc bugfixes. * Misc bugfixes.
### r2.5.4 ### ### r2.5.4 (2017-10-19) ###
* Remove unnecessary media playlist fetches during playback of live HLS streams. * Remove unnecessary media playlist fetches during playback of live HLS streams.
* Add the ability to inject a HLS playlist parser through `HlsMediaSource`. * Add the ability to inject a HLS playlist parser through `HlsMediaSource`.
@ -851,7 +1408,7 @@
`FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk`
non-final. non-final.
### r2.5.3 ### ### r2.5.3 (2017-09-20) ###
* IMA extension: Support skipping of skippable ads on AndroidTV and other * IMA extension: Support skipping of skippable ads on AndroidTV and other
non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)). non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)).
@ -866,7 +1423,7 @@
* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue * Caching: Force use of BouncyCastle on JellyBean to fix decryption issue
([#2755](https://github.com/google/ExoPlayer/issues/2755)). ([#2755](https://github.com/google/ExoPlayer/issues/2755)).
### r2.5.2 ### ### r2.5.2 (2017-09-11) ###
* IMA extension: Fix issue where ad playback could end prematurely for some * IMA extension: Fix issue where ad playback could end prematurely for some
content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)).
@ -892,14 +1449,14 @@
* Workaround for SimpleExoPlayerView's surface not being hidden properly * Workaround for SimpleExoPlayerView's surface not being hidden properly
([#3160](https://github.com/google/ExoPlayer/issues/3160)). ([#3160](https://github.com/google/ExoPlayer/issues/3160)).
### r2.5.1 ### ### r2.5.1 (2017-08-08) ###
* Fix an issue that could cause the reported playback position to stop advancing * Fix an issue that could cause the reported playback position to stop advancing
in some cases. in some cases.
* Fix an issue where a Surface could be released whilst still in use by the * Fix an issue where a Surface could be released whilst still in use by the
player. player.
### r2.5.0 ### ### r2.5.0 (2017-08-07) ###
* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an
easy and seamless way of incorporating display ads into ExoPlayer playbacks. easy and seamless way of incorporating display ads into ExoPlayer playbacks.
@ -953,7 +1510,7 @@
media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)).
* Misc bugfixes. * Misc bugfixes.
### r2.4.4 ### ### r2.4.4 (2017-07-19) ###
* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance * HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance
([#3040](https://github.com/google/ExoPlayer/issues/3040)). ([#3040](https://github.com/google/ExoPlayer/issues/3040)).
@ -964,7 +1521,7 @@
* Video: Fix video dimension reporting on some devices * Video: Fix video dimension reporting on some devices
([#3007](https://github.com/google/ExoPlayer/issues/3007)). ([#3007](https://github.com/google/ExoPlayer/issues/3007)).
### r2.4.3 ### ### r2.4.3 (2017-06-30) ###
* Audio: Workaround custom audio decoders misreporting their maximum supported * Audio: Workaround custom audio decoders misreporting their maximum supported
channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)).
@ -982,7 +1539,7 @@
([#2977](https://github.com/google/ExoPlayer/pull/2977)). ([#2977](https://github.com/google/ExoPlayer/pull/2977)).
* Misc bugfixes. * Misc bugfixes.
### r2.4.2 ### ### r2.4.2 (2017-06-06) ###
* Stability: Work around Nexus 10 reboot when playing certain content * Stability: Work around Nexus 10 reboot when playing certain content
([#2806](https://github.com/google/ExoPlayer/issues/2806)). ([#2806](https://github.com/google/ExoPlayer/issues/2806)).
@ -996,7 +1553,7 @@
([#2871](https://github.com/google/ExoPlayer/issues/2871)). ([#2871](https://github.com/google/ExoPlayer/issues/2871)).
* Misc bugfixes. * Misc bugfixes.
### r2.4.1 ### ### r2.4.1 (2017-05-23) ###
* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media * Stability: Avoid OutOfMemoryError in extractors when parsing malformed media
([#2780](https://github.com/google/ExoPlayer/issues/2780)). ([#2780](https://github.com/google/ExoPlayer/issues/2780)).
@ -1019,7 +1576,7 @@
([#2824](https://github.com/google/ExoPlayer/issues/2824)). ([#2824](https://github.com/google/ExoPlayer/issues/2824)).
* Misc bugfixes. * Misc bugfixes.
### r2.4.0 ### ### r2.4.0 (2017-04-25) ###
* New modular library structure. You can read more about depending on individual * New modular library structure. You can read more about depending on individual
library modules library modules
@ -1052,7 +1609,7 @@
* FLAC extension: Enabled 64 bit targets. * FLAC extension: Enabled 64 bit targets.
* Misc bugfixes. * Misc bugfixes.
### r2.3.1 ### ### r2.3.1 (2017-03-23) ###
* Fix NPE enabling WebVTT subtitles in DASH streams * Fix NPE enabling WebVTT subtitles in DASH streams
([#2596](https://github.com/google/ExoPlayer/issues/2596)). ([#2596](https://github.com/google/ExoPlayer/issues/2596)).
@ -1061,7 +1618,7 @@
* Minor fix for CEA-708 decoder * Minor fix for CEA-708 decoder
([#2595](https://github.com/google/ExoPlayer/issues/2595)). ([#2595](https://github.com/google/ExoPlayer/issues/2595)).
### r2.3.0 ### ### r2.3.0 (2017-03-16) ###
* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio * GVR extension: Wraps the Google VR Audio SDK to provide spatial audio
rendering. You can read more about the GVR extension rendering. You can read more about the GVR extension
@ -1108,7 +1665,7 @@
([#2427](https://github.com/google/ExoPlayer/issues/2427)). ([#2427](https://github.com/google/ExoPlayer/issues/2427)).
* Misc bugfixes. * Misc bugfixes.
### r2.2.0 ### ### r2.2.0 (2017-01-30) ###
* Demo app: Automatic recovery from BehindLiveWindowException, plus improved * Demo app: Automatic recovery from BehindLiveWindowException, plus improved
handling of pausing and resuming live streams handling of pausing and resuming live streams
@ -1171,7 +1728,7 @@
[#2264](https://github.com/google/ExoPlayer/issues/2264) and [#2264](https://github.com/google/ExoPlayer/issues/2264) and
[#2290](https://github.com/google/ExoPlayer/issues/2290). [#2290](https://github.com/google/ExoPlayer/issues/2290).
### r2.1.1 ### ### r2.1.1 (2016-12-20) ###
* Fix some subtitle types (e.g. WebVTT) being displayed out of sync * Fix some subtitle types (e.g. WebVTT) being displayed out of sync
([#2208](https://github.com/google/ExoPlayer/issues/2208)). ([#2208](https://github.com/google/ExoPlayer/issues/2208)).
@ -1181,7 +1738,7 @@
* Fix issue where playbacks could get stuck in the initial buffering state if * Fix issue where playbacks could get stuck in the initial buffering state if
over 1MB of data needs to be read to initialize the playback. over 1MB of data needs to be read to initialize the playback.
### r2.1.0 ### ### r2.1.0 (2016-12-14) ###
* HLS: Support for seeking in live streams * HLS: Support for seeking in live streams
([#87](https://github.com/google/ExoPlayer/issues/87)). ([#87](https://github.com/google/ExoPlayer/issues/87)).
@ -1235,12 +1792,12 @@
([#2145](https://github.com/google/ExoPlayer/issues/2145)). ([#2145](https://github.com/google/ExoPlayer/issues/2145)).
* Misc bugfixes. * Misc bugfixes.
### r2.0.4 ### ### r2.0.4 (2016-10-20) ###
* Fix crash on Jellybean devices when using playback controls * Fix crash on Jellybean devices when using playback controls
([#1965](https://github.com/google/ExoPlayer/issues/1965)). ([#1965](https://github.com/google/ExoPlayer/issues/1965)).
### r2.0.3 ### ### r2.0.3 (2016-10-17) ###
* Fixed NullPointerException in ExtractorMediaSource * Fixed NullPointerException in ExtractorMediaSource
([#1914](https://github.com/google/ExoPlayer/issues/1914)). ([#1914](https://github.com/google/ExoPlayer/issues/1914)).
@ -1257,7 +1814,7 @@
* Improvements to Cronet network stack extension. * Improvements to Cronet network stack extension.
* Misc bug fixes. * Misc bug fixes.
### r2.0.2 ### ### r2.0.2 (2016-10-06) ###
* Fixes for MergingMediaSource and sideloaded subtitles. * Fixes for MergingMediaSource and sideloaded subtitles.
([#1882](https://github.com/google/ExoPlayer/issues/1882), ([#1882](https://github.com/google/ExoPlayer/issues/1882),
@ -1268,7 +1825,7 @@
* Initial support for fragmented MP4 in HLS. * Initial support for fragmented MP4 in HLS.
* Misc bug fixes and minor features. * Misc bug fixes and minor features.
### r2.0.1 ### ### r2.0.1 (2016-09-30) ###
* Fix playback of short duration content * Fix playback of short duration content
([#1837](https://github.com/google/ExoPlayer/issues/1837)). ([#1837](https://github.com/google/ExoPlayer/issues/1837)).
@ -1277,7 +1834,7 @@
* Fix live stream buffering (out of memory) issue * Fix live stream buffering (out of memory) issue
([#1825](https://github.com/google/ExoPlayer/issues/1825)). ([#1825](https://github.com/google/ExoPlayer/issues/1825)).
### r2.0.0 ### ### r2.0.0 (2016-09-14) ###
ExoPlayer 2.x is a major iteration of the library. It includes significant API ExoPlayer 2.x is a major iteration of the library. It includes significant API
and architectural changes, new features and many bug fixes. You can read about and architectural changes, new features and many bug fixes. You can read about

View File

@ -17,17 +17,9 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.4' classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.novoda:bintray-release:0.8.1' classpath 'com.novoda:bintray-release:0.9.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.0.3' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.0'
}
// Workaround for the following test coverage issue. Remove when fixed:
// https://code.google.com/p/android/issues/detail?id=226070
configurations.all {
resolutionStrategy {
force 'org.jacoco:org.jacoco.report:0.7.4.201502262128'
force 'org.jacoco:org.jacoco.core:0.7.4.201502262128'
}
} }
} }
allprojects { allprojects {
@ -36,7 +28,7 @@ allprojects {
jcenter() jcenter()
} }
project.ext { project.ext {
exoplayerPublishEnabled = true exoplayerPublishEnabled = false
} }
if (it.hasProperty('externalBuildDir')) { if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) { if (!new File(externalBuildDir).isAbsolute()) {
@ -44,6 +36,7 @@ allprojects {
} }
buildDir = "${externalBuildDir}/${project.name}" buildDir = "${externalBuildDir}/${project.name}"
} }
group = 'com.google.android.exoplayer'
} }
apply from: 'javadoc_combined.gradle' apply from: 'javadoc_combined.gradle'

View File

@ -13,26 +13,29 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.9.4' releaseVersion = '2.11.1'
releaseVersionCode = 2009004 releaseVersionCode = 2011001
// Important: ExoPlayer specifies a minSdkVersion of 14 because various minSdkVersion = 16
// components provided by the library may be of use on older devices. appTargetSdkVersion = 29
// However, please note that the core media playback functionality provided targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
// by the library requires API level 16 or greater. compileSdkVersion = 29
minSdkVersion = 14 dexmakerVersion = '2.21.0'
targetSdkVersion = 28 junitVersion = '4.13-rc-2'
compileSdkVersion = 28 guavaVersion = '23.5-android'
buildToolsVersion = '28.0.2' mockitoVersion = '2.25.0'
testSupportLibraryVersion = '0.5' robolectricVersion = '4.3.1'
supportLibraryVersion = '27.1.1'
dexmakerVersion = '1.2'
mockitoVersion = '1.9.5'
junitVersion = '4.12'
truthVersion = '0.39'
robolectricVersion = '3.7.1'
autoValueVersion = '1.6'
checkerframeworkVersion = '2.5.0' checkerframeworkVersion = '2.5.0'
testRunnerVersion = '1.1.0-alpha3' jsr305Version = '3.0.2'
kotlinAnnotationsVersion = '1.3.31'
androidxAnnotationVersion = '1.1.0'
androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1'
androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0'
androidxTestRulesVersion = '1.2.0'
truthVersion = '1.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
modulePrefix += gradle.ext.exoplayerModulePrefix modulePrefix += gradle.ext.exoplayerModulePrefix

View File

@ -24,7 +24,7 @@ include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-ui' include modulePrefix + 'library-ui'
include modulePrefix + 'testutils' include modulePrefix + 'testutils'
include modulePrefix + 'testutils-robolectric' include modulePrefix + 'extension-av1'
include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-ffmpeg'
include modulePrefix + 'extension-flac' include modulePrefix + 'extension-flac'
include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-gvr'
@ -38,6 +38,7 @@ include modulePrefix + 'extension-vp9'
include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-rtmp'
include modulePrefix + 'extension-leanback' include modulePrefix + 'extension-leanback'
include modulePrefix + 'extension-jobdispatcher' include modulePrefix + 'extension-jobdispatcher'
include modulePrefix + 'extension-workmanager'
project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all')
project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core')
@ -46,7 +47,7 @@ project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hl
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
project(modulePrefix + 'testutils-robolectric').projectDir = new File(rootDir, 'testutils_robolectric') project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg') project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'extensions/ffmpeg')
project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac')
project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr')
@ -60,3 +61,4 @@ project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensio
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')

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -26,8 +25,8 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
@ -45,20 +44,9 @@ android {
} }
lintOptions { lintOptions {
// The demo app does not have translations. // The demo app isn't indexed and doesn't have translations.
disable 'MissingTranslation' disable 'GoogleAppIndexingWarning','MissingTranslation'
} }
flavorDimensions "receiver"
productFlavors {
defaultCast {
dimension "receiver"
manifestPlaceholders =
[castOptionsProvider: "com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"]
}
}
} }
dependencies { dependencies {
@ -68,9 +56,9 @@ dependencies {
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast') implementation project(modulePrefix + 'extension-cast')
implementation 'com.android.support:support-v4:' + supportLibraryVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion implementation 'com.google.android.material:material:1.0.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

@ -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 {
*; *;
} }

View File

@ -17,13 +17,15 @@
package="com.google.android.exoplayer2.castdemo"> package="com.google.android.exoplayer2.castdemo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/> <uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false"> android:largeHeap="true" android:allowBackup="false">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="${castOptionsProvider}" /> android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<activity android:name="com.google.android.exoplayer2.castdemo.MainActivity" <activity android:name="com.google.android.exoplayer2.castdemo.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View File

@ -1,408 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList;
/** Manages players and an internal media queue for the ExoPlayer/Cast demo app. */
/* package */ class DefaultReceiverPlayerManager
implements EventListener, SessionAvailabilityListener, PlayerManager {
private static final String USER_AGENT = "ExoCastDemoPlayer";
private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT);
private final PlayerView localPlayerView;
private final PlayerControlView castControlView;
private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer;
private final ArrayList<MediaItem> mediaQueue;
private final QueuePositionListener queuePositionListener;
private final ConcatenatingMediaSource concatenatingMediaSource;
private boolean castMediaQueueCreationPending;
private int currentItemIndex;
private Player currentPlayer;
/**
* @param queuePositionListener A {@link QueuePositionListener} for queue position changes.
* @param localPlayerView The {@link PlayerView} for local playback.
* @param castControlView The {@link PlayerControlView} to control remote playback.
* @param context A {@link Context}.
* @param castContext The {@link CastContext}.
*/
public static DefaultReceiverPlayerManager createPlayerManager(
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
DefaultReceiverPlayerManager defaultReceiverPlayerManager =
new DefaultReceiverPlayerManager(
queuePositionListener, localPlayerView, castControlView, context, castContext);
defaultReceiverPlayerManager.init();
return defaultReceiverPlayerManager;
}
private DefaultReceiverPlayerManager(
QueuePositionListener queuePositionListener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
this.queuePositionListener = queuePositionListener;
this.localPlayerView = localPlayerView;
this.castControlView = castControlView;
mediaQueue = new ArrayList<>();
currentItemIndex = C.INDEX_UNSET;
concatenatingMediaSource = new ConcatenatingMediaSource();
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
exoPlayer = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector);
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
castPlayer = new CastPlayer(castContext);
castPlayer.addListener(this);
castPlayer.setSessionAvailabilityListener(this);
castControlView.setPlayer(castPlayer);
}
// Queue manipulation methods.
/**
* Plays a specified queue item in the current player.
*
* @param itemIndex The index of the item to play.
*/
public void selectQueueItem(int itemIndex) {
setCurrentItem(itemIndex, C.TIME_UNSET, true);
}
/**
* Returns the index of the currently played item.
*/
public int getCurrentItemIndex() {
return currentItemIndex;
}
/**
* Appends {@code item} to the media queue.
*
* @param item The {@link MediaItem} to append.
*/
public void addItem(MediaItem item) {
mediaQueue.add(item);
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
if (currentPlayer == castPlayer) {
castPlayer.addItems(buildMediaQueueItem(item));
}
}
/**
* Returns the size of the media queue.
*/
public int getMediaQueueSize() {
return mediaQueue.size();
}
/**
* Returns the item at the given index in the media queue.
*
* @param position The index of the item.
* @return The item at the given index in the media queue.
*/
public MediaItem getItem(int position) {
return mediaQueue.get(position);
}
/**
* Removes the item at the given index from the media queue.
*
* @param itemIndex The index of the item to remove.
* @return Whether the removal was successful.
*/
public boolean removeItem(int itemIndex) {
concatenatingMediaSource.removeMediaSource(itemIndex);
if (currentPlayer == castPlayer) {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
if (castTimeline.getPeriodCount() <= itemIndex) {
return false;
}
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
}
}
mediaQueue.remove(itemIndex);
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
} else if (itemIndex < currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
}
return true;
}
/**
* Moves an item within the queue.
*
* @param fromIndex The index of the item to move.
* @param toIndex The target index of the item in the queue.
* @return Whether the item move was successful.
*/
public boolean moveItem(int fromIndex, int toIndex) {
// Player update.
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount();
if (periodCount <= fromIndex || periodCount <= toIndex) {
return false;
}
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
castPlayer.moveItem(elementId, toIndex);
}
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
// Index update.
if (fromIndex == currentItemIndex) {
maybeSetCurrentItemAndNotify(toIndex);
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
}
return true;
}
// Miscellaneous methods.
/**
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
*
* @param event The {@link KeyEvent}.
* @return Whether the event was handled by the target view.
*/
public boolean dispatchKeyEvent(KeyEvent event) {
if (currentPlayer == exoPlayer) {
return localPlayerView.dispatchKeyEvent(event);
} else /* currentPlayer == castPlayer */ {
return castControlView.dispatchKeyEvent(event);
}
}
/**
* Releases the manager and the players that it holds.
*/
public void release() {
currentItemIndex = C.INDEX_UNSET;
mediaQueue.clear();
concatenatingMediaSource.clear();
castPlayer.setSessionAvailabilityListener(null);
castPlayer.release();
localPlayerView.setPlayer(null);
exoPlayer.release();
}
// Player.EventListener implementation.
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
updateCurrentItemIndex();
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (currentPlayer == castPlayer && timeline.isEmpty()) {
castMediaQueueCreationPending = true;
}
}
// CastPlayer.SessionAvailabilityListener implementation.
@Override
public void onCastSessionAvailable() {
setCurrentPlayer(castPlayer);
}
@Override
public void onCastSessionUnavailable() {
setCurrentPlayer(exoPlayer);
}
// Internal methods.
private void init() {
setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
}
private void updateCurrentItemIndex() {
int playbackState = currentPlayer.getPlaybackState();
maybeSetCurrentItemAndNotify(
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET);
}
private void setCurrentPlayer(Player currentPlayer) {
if (this.currentPlayer == currentPlayer) {
return;
}
// View management.
if (currentPlayer == exoPlayer) {
localPlayerView.setVisibility(View.VISIBLE);
castControlView.hide();
} else /* currentPlayer == castPlayer */ {
localPlayerView.setVisibility(View.GONE);
castControlView.show();
}
// Player state management.
long playbackPositionMs = C.TIME_UNSET;
int windowIndex = C.INDEX_UNSET;
boolean playWhenReady = false;
if (this.currentPlayer != null) {
int playbackState = this.currentPlayer.getPlaybackState();
if (playbackState != Player.STATE_ENDED) {
playbackPositionMs = this.currentPlayer.getCurrentPosition();
playWhenReady = this.currentPlayer.getPlayWhenReady();
windowIndex = this.currentPlayer.getCurrentWindowIndex();
if (windowIndex != currentItemIndex) {
playbackPositionMs = C.TIME_UNSET;
windowIndex = currentItemIndex;
}
}
this.currentPlayer.stop(true);
} else {
// This is the initial setup. No need to save any state.
}
this.currentPlayer = currentPlayer;
// Media queue management.
castMediaQueueCreationPending = currentPlayer == castPlayer;
if (currentPlayer == exoPlayer) {
exoPlayer.prepare(concatenatingMediaSource);
}
// Playback transition.
if (windowIndex != C.INDEX_UNSET) {
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
}
}
/**
* Starts playback of the item at the given position.
*
* @param itemIndex The index of the item to play.
* @param positionMs The position at which playback should start.
* @param playWhenReady Whether the player should proceed when ready to do so.
*/
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
maybeSetCurrentItemAndNotify(itemIndex);
if (castMediaQueueCreationPending) {
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
for (int i = 0; i < items.length; i++) {
items[i] = buildMediaQueueItem(mediaQueue.get(i));
}
castMediaQueueCreationPending = false;
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
} else {
currentPlayer.seekTo(itemIndex, positionMs);
currentPlayer.setPlayWhenReady(playWhenReady);
}
}
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
if (this.currentItemIndex != currentItemIndex) {
int oldIndex = this.currentItemIndex;
this.currentItemIndex = currentItemIndex;
queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex);
}
}
private static MediaSource buildMediaSource(MediaItem item) {
Uri uri = item.media.uri;
switch (item.mimeType) {
case DemoUtil.MIME_TYPE_SS:
return new SsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_DASH:
return new DashMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_HLS:
return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
case DemoUtil.MIME_TYPE_VIDEO_MP4:
return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri);
default: {
throw new IllegalStateException("Unsupported type: " + item.mimeType);
}
}
}
private static MediaQueueItem buildMediaQueueItem(MediaItem item) {
MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
movieMetadata.putString(MediaMetadata.KEY_TITLE, item.title);
MediaInfo mediaInfo =
new MediaInfo.Builder(item.media.uri.toString())
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(item.mimeType)
.setMetadata(movieMetadata)
.build();
return new MediaQueueItem.Builder(mediaInfo).build();
}
}

View File

@ -16,98 +16,87 @@
package com.google.android.exoplayer2.castdemo; package com.google.android.exoplayer2.castdemo;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.MediaItem.DrmConfiguration;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID;
/** Utility methods and constants for the Cast demo application. */ /** Utility methods and constants for the Cast demo application. */
/* package */ final class DemoUtil { /* package */ final class DemoUtil {
/** Represents a media sample. */
public static final class Sample {
/** The uri of the media content. */
public final String uri;
/** The name of the sample. */
public final String name;
/** The mime type of the sample media content. */
public final String mimeType;
/**
* The {@link UUID} of the DRM scheme that protects the content, or null if the content is not
* DRM-protected.
*/
@Nullable public final UUID drmSchemeUuid;
/**
* The url from which players should obtain DRM licenses, or null if the content is not
* DRM-protected.
*/
@Nullable public final Uri licenseServerUri;
/**
* @param uri See {@link #uri}.
* @param name See {@link #name}.
* @param mimeType See {@link #mimeType}.
*/
public Sample(String uri, String name, String mimeType) {
this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null);
}
public Sample(
String uri,
String name,
String mimeType,
@Nullable UUID drmSchemeUuid,
@Nullable String licenseServerUriString) {
this.uri = uri;
this.name = name;
this.mimeType = mimeType;
this.drmSchemeUuid = drmSchemeUuid;
this.licenseServerUri =
licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null;
}
@Override
public String toString() {
return name;
}
}
public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD;
public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8;
public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS;
public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4;
/** The list of samples available in the cast demo app. */ /** The list of samples available in the cast demo app. */
public static final List<Sample> SAMPLES; public static final List<MediaItem> SAMPLES;
static { static {
// App samples. ArrayList<MediaItem> samples = new ArrayList<>();
ArrayList<Sample> samples = new ArrayList<>();
// Clear content.
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", .setUri("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd")
"Clear DASH: Tears", .setTitle("Clear DASH: Tears")
MIME_TYPE_DASH)); .setMimeType(MIME_TYPE_DASH)
.build());
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8")
+ "hls/TearsOfSteel.m3u8", .setTitle("Clear HLS: Angel one")
"Clear HLS: Tears of Steel", .setMimeType(MIME_TYPE_HLS)
MIME_TYPE_HLS)); .build());
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3" .setUri("https://html5demos.com/assets/dizzy.mp4")
+ "/bipbop_4x3_variant.m3u8", .setTitle("Clear MP4: Dizzy")
"Clear HLS: Basic 4x3", .setMimeType(MIME_TYPE_VIDEO_MP4)
MIME_TYPE_HLS)); .build());
// DRM content.
samples.add( samples.add(
new Sample( new MediaItem.Builder()
"https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); .setUri(Uri.parse("https://storage.googleapis.com/wvmedia/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);
} }

View File

@ -17,13 +17,6 @@ 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 android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.helper.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,25 +26,31 @@ 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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
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.ext.cast.MediaItem; import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.dynamite.DynamiteModule; import com.google.android.gms.dynamite.DynamiteModule;
import java.util.Collections;
/** /**
* An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's
* Cast extension. * Cast extension.
*/ */
public class MainActivity extends AppCompatActivity public class MainActivity extends AppCompatActivity
implements OnClickListener, PlayerManager.QueuePositionListener { implements OnClickListener, PlayerManager.Listener {
private final MediaItem.Builder mediaItemBuilder;
private PlayerView localPlayerView; private PlayerView localPlayerView;
private PlayerControlView castControlView; private PlayerControlView castControlView;
@ -60,10 +59,6 @@ public class MainActivity extends AppCompatActivity
private MediaQueueListAdapter mediaQueueListAdapter; private MediaQueueListAdapter mediaQueueListAdapter;
private CastContext castContext; private CastContext castContext;
public MainActivity() {
mediaItemBuilder = new MediaItem.Builder();
}
// Activity lifecycle methods. // Activity lifecycle methods.
@Override @Override
@ -76,7 +71,7 @@ public class MainActivity extends AppCompatActivity
Throwable cause = e.getCause(); Throwable cause = e.getCause();
while (cause != null) { while (cause != null) {
if (cause instanceof DynamiteModule.LoadingException) { if (cause instanceof DynamiteModule.LoadingException) {
setContentView(R.layout.cast_context_error_message_layout); setContentView(R.layout.cast_context_error);
return; return;
} }
cause = cause.getCause(); cause = cause.getCause();
@ -117,20 +112,13 @@ public class MainActivity extends AppCompatActivity
// There is no Cast context to work with. Do nothing. // There is no Cast context to work with. Do nothing.
return; return;
} }
String applicationId = castContext.getCastOptions().getReceiverApplicationId(); playerManager =
switch (applicationId) { new PlayerManager(
case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: /* listener= */ this,
playerManager = localPlayerView,
DefaultReceiverPlayerManager.createPlayerManager( castControlView,
/* queuePositionListener= */ this, /* context= */ this,
localPlayerView, castContext);
castControlView,
/* context= */ this,
castContext);
break;
default:
throw new IllegalStateException("Illegal receiver app id: " + applicationId);
}
mediaQueueList.setAdapter(mediaQueueListAdapter); mediaQueueList.setAdapter(mediaQueueListAdapter);
} }
@ -157,12 +145,15 @@ public class MainActivity extends AppCompatActivity
@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) {
@ -174,27 +165,30 @@ public class MainActivity extends AppCompatActivity
} }
} }
@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);
sampleList.setAdapter(new SampleListAdapter(this)); sampleList.setAdapter(new SampleListAdapter(this));
sampleList.setOnItemClickListener( sampleList.setOnItemClickListener(
(parent, view, position, id) -> { (parent, view, position, id) -> {
DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); playerManager.addItem(DemoUtil.SAMPLES.get(position));
mediaItemBuilder
.clear()
.setMedia(sample.uri)
.setTitle(sample.name)
.setMimeType(sample.mimeType);
if (sample.drmSchemeUuid != null) {
mediaItemBuilder.setDrmSchemes(
Collections.singletonList(
new MediaItem.DrmScheme(
sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri))));
}
playerManager.addItem(mediaItemBuilder.build());
mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1);
}); });
return dialogList; return dialogList;
@ -202,23 +196,6 @@ public class MainActivity extends AppCompatActivity
// 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
@ -230,11 +207,14 @@ public class MainActivity extends AppCompatActivity
@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).title); view.setText(holder.item.title);
// TODO: Solve coloring using the theme's ColorStateList. // TODO: Solve coloring using the theme's ColorStateList.
view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), view.setTextColor(
position == playerManager.getCurrentItemIndex() ? 255 : 100)); ColorUtils.setAlphaComponent(
view.getCurrentTextColor(),
position == playerManager.getCurrentItemIndex() ? 255 : 100));
} }
@Override @Override
@ -272,8 +252,11 @@ public class MainActivity extends AppCompatActivity
@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);
} }
} }
@ -281,8 +264,9 @@ public class MainActivity extends AppCompatActivity
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();
@ -291,14 +275,37 @@ public class MainActivity extends AppCompatActivity
draggingFromPosition = C.INDEX_UNSET; draggingFromPosition = C.INDEX_UNSET;
draggingToPosition = C.INDEX_UNSET; draggingToPosition = C.INDEX_UNSET;
} }
} }
private static final class SampleListAdapter extends ArrayAdapter<DemoUtil.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;
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 The Android Open Source Project * Copyright (C) 2019 The Android Open Source Project
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,50 +15,445 @@
*/ */
package com.google.android.exoplayer2.castdemo; package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ext.cast.MediaItem;
import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.framework.CastContext;
import java.util.ArrayList;
import java.util.Map;
/** Manages the players in the Cast demo app. */ /** Manages players and an internal media queue for the demo app. */
interface PlayerManager { /* package */ class PlayerManager implements EventListener, SessionAvailabilityListener {
/** Listener for changes in the media queue playback position. */ /** Listener for events. */
interface QueuePositionListener { interface Listener {
/** /** 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);
} }
/** Redirects the given {@code keyEvent} to the active player. */ private static final String USER_AGENT = "ExoCastDemoPlayer";
boolean dispatchKeyEvent(KeyEvent keyEvent); private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY =
new DefaultHttpDataSourceFactory(USER_AGENT);
/** Appends the given {@link MediaItem} to the media queue. */ private final PlayerView localPlayerView;
void addItem(MediaItem mediaItem); private final PlayerControlView castControlView;
private final DefaultTrackSelector trackSelector;
private final SimpleExoPlayer exoPlayer;
private final CastPlayer castPlayer;
private final ArrayList<MediaItem> mediaQueue;
private final Listener listener;
private final ConcatenatingMediaSource concatenatingMediaSource;
private final MediaItemConverter mediaItemConverter;
/** Returns the number of items in the media queue. */ private TrackGroupArray lastSeenTrackGroupArray;
int getMediaQueueSize(); private int currentItemIndex;
private Player currentPlayer;
/** Selects the item at the given position for playback. */
void selectQueueItem(int position);
/** /**
* Returns the position of the item currently being played, or {@link C#INDEX_UNSET} if no item is * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}.
* being played. *
* @param listener A {@link Listener} for queue position changes.
* @param localPlayerView The {@link PlayerView} for local playback.
* @param castControlView The {@link PlayerControlView} to control remote playback.
* @param context A {@link Context}.
* @param castContext The {@link CastContext}.
*/ */
int getCurrentItemIndex(); public PlayerManager(
Listener listener,
PlayerView localPlayerView,
PlayerControlView castControlView,
Context context,
CastContext castContext) {
this.listener = listener;
this.localPlayerView = localPlayerView;
this.castControlView = castControlView;
mediaQueue = new ArrayList<>();
currentItemIndex = C.INDEX_UNSET;
concatenatingMediaSource = new ConcatenatingMediaSource();
mediaItemConverter = new DefaultMediaItemConverter();
/** Returns the {@link MediaItem} at the given {@code position}. */ trackSelector = new DefaultTrackSelector(context);
MediaItem getItem(int position); exoPlayer = new SimpleExoPlayer.Builder(context).setTrackSelector(trackSelector).build();
exoPlayer.addListener(this);
localPlayerView.setPlayer(exoPlayer);
/** Moves the item at position {@code from} to position {@code to}. */ castPlayer = new CastPlayer(castContext);
boolean moveItem(int from, int to); castPlayer.addListener(this);
castPlayer.setSessionAvailabilityListener(this);
castControlView.setPlayer(castPlayer);
/** Removes the item at position {@code index}. */ setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer);
boolean removeItem(int index); }
/** Releases any acquired resources. */ // Queue manipulation methods.
void release();
/**
* Plays a specified queue item in the current player.
*
* @param itemIndex The index of the item to play.
*/
public void selectQueueItem(int itemIndex) {
setCurrentItem(itemIndex, C.TIME_UNSET, true);
}
/** Returns the index of the currently played item. */
public int getCurrentItemIndex() {
return currentItemIndex;
}
/**
* Appends {@code item} to the media queue.
*
* @param item The {@link MediaItem} to append.
*/
public void addItem(MediaItem item) {
mediaQueue.add(item);
concatenatingMediaSource.addMediaSource(buildMediaSource(item));
if (currentPlayer == castPlayer) {
castPlayer.addItems(mediaItemConverter.toMediaQueueItem(item));
}
}
/** Returns the size of the media queue. */
public int getMediaQueueSize() {
return mediaQueue.size();
}
/**
* Returns the item at the given index in the media queue.
*
* @param position The index of the item.
* @return The item at the given index in the media queue.
*/
public MediaItem getItem(int position) {
return mediaQueue.get(position);
}
/**
* Removes the item at the given index from the media queue.
*
* @param item The item to remove.
* @return Whether the removal was successful.
*/
public boolean removeItem(MediaItem item) {
int itemIndex = mediaQueue.indexOf(item);
if (itemIndex == -1) {
return false;
}
concatenatingMediaSource.removeMediaSource(itemIndex);
if (currentPlayer == castPlayer) {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
if (castTimeline.getPeriodCount() <= itemIndex) {
return false;
}
castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id);
}
}
mediaQueue.remove(itemIndex);
if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) {
maybeSetCurrentItemAndNotify(C.INDEX_UNSET);
} else if (itemIndex < currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
}
return true;
}
/**
* Moves an item within the queue.
*
* @param item The item to move.
* @param toIndex The target index of the item in the queue.
* @return Whether the item move was successful.
*/
public boolean moveItem(MediaItem item, int toIndex) {
int fromIndex = mediaQueue.indexOf(item);
if (fromIndex == -1) {
return false;
}
// Player update.
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
if (currentPlayer == castPlayer && castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount();
if (periodCount <= fromIndex || periodCount <= toIndex) {
return false;
}
int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id;
castPlayer.moveItem(elementId, toIndex);
}
mediaQueue.add(toIndex, mediaQueue.remove(fromIndex));
// Index update.
if (fromIndex == currentItemIndex) {
maybeSetCurrentItemAndNotify(toIndex);
} else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex - 1);
} else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) {
maybeSetCurrentItemAndNotify(currentItemIndex + 1);
}
return true;
}
/**
* Dispatches a given {@link KeyEvent} to the corresponding view of the current player.
*
* @param event The {@link KeyEvent}.
* @return Whether the event was handled by the target view.
*/
public boolean dispatchKeyEvent(KeyEvent event) {
if (currentPlayer == exoPlayer) {
return localPlayerView.dispatchKeyEvent(event);
} else /* currentPlayer == castPlayer */ {
return castControlView.dispatchKeyEvent(event);
}
}
/** Releases the manager and the players that it holds. */
public void release() {
currentItemIndex = C.INDEX_UNSET;
mediaQueue.clear();
concatenatingMediaSource.clear();
castPlayer.setSessionAvailabilityListener(null);
castPlayer.release();
localPlayerView.setPlayer(null);
exoPlayer.release();
}
// Player.EventListener implementation.
@Override
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
updateCurrentItemIndex();
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
if (currentPlayer == exoPlayer && trackGroups != lastSeenTrackGroupArray) {
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
}
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
== MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
}
}
lastSeenTrackGroupArray = trackGroups;
}
}
// CastPlayer.SessionAvailabilityListener implementation.
@Override
public void onCastSessionAvailable() {
setCurrentPlayer(castPlayer);
}
@Override
public void onCastSessionUnavailable() {
setCurrentPlayer(exoPlayer);
}
// Internal methods.
private void updateCurrentItemIndex() {
int playbackState = currentPlayer.getPlaybackState();
maybeSetCurrentItemAndNotify(
playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED
? currentPlayer.getCurrentWindowIndex()
: C.INDEX_UNSET);
}
private void setCurrentPlayer(Player currentPlayer) {
if (this.currentPlayer == currentPlayer) {
return;
}
// View management.
if (currentPlayer == exoPlayer) {
localPlayerView.setVisibility(View.VISIBLE);
castControlView.hide();
} else /* currentPlayer == castPlayer */ {
localPlayerView.setVisibility(View.GONE);
castControlView.show();
}
// Player state management.
long playbackPositionMs = C.TIME_UNSET;
int windowIndex = C.INDEX_UNSET;
boolean playWhenReady = false;
Player previousPlayer = this.currentPlayer;
if (previousPlayer != null) {
// Save state from the previous player.
int playbackState = previousPlayer.getPlaybackState();
if (playbackState != Player.STATE_ENDED) {
playbackPositionMs = previousPlayer.getCurrentPosition();
playWhenReady = previousPlayer.getPlayWhenReady();
windowIndex = previousPlayer.getCurrentWindowIndex();
if (windowIndex != currentItemIndex) {
playbackPositionMs = C.TIME_UNSET;
windowIndex = currentItemIndex;
}
}
previousPlayer.stop(true);
}
this.currentPlayer = currentPlayer;
// Media queue management.
if (currentPlayer == exoPlayer) {
exoPlayer.prepare(concatenatingMediaSource);
}
// Playback transition.
if (windowIndex != C.INDEX_UNSET) {
setCurrentItem(windowIndex, playbackPositionMs, playWhenReady);
}
}
/**
* Starts playback of the item at the given position.
*
* @param itemIndex The index of the item to play.
* @param positionMs The position at which playback should start.
* @param playWhenReady Whether the player should proceed when ready to do so.
*/
private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) {
maybeSetCurrentItemAndNotify(itemIndex);
if (currentPlayer == castPlayer && castPlayer.getCurrentTimeline().isEmpty()) {
MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()];
for (int i = 0; i < items.length; i++) {
items[i] = mediaItemConverter.toMediaQueueItem(mediaQueue.get(i));
}
castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF);
} else {
currentPlayer.seekTo(itemIndex, positionMs);
currentPlayer.setPlayWhenReady(playWhenReady);
}
}
private void maybeSetCurrentItemAndNotify(int currentItemIndex) {
if (this.currentItemIndex != currentItemIndex) {
int oldIndex = this.currentItemIndex;
this.currentItemIndex = currentItemIndex;
listener.onQueuePositionChanged(oldIndex, currentItemIndex);
}
}
private MediaSource buildMediaSource(MediaItem item) {
Uri uri = item.uri;
String mimeType = item.mimeType;
if (mimeType == null) {
throw new IllegalArgumentException("mimeType is required");
}
DrmSessionManager<ExoMediaCrypto> drmSessionManager =
DrmSessionManager.getDummyDrmSessionManager();
MediaItem.DrmConfiguration drmConfiguration = item.drmConfiguration;
if (drmConfiguration != null && Util.SDK_INT >= 18) {
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());
}
drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setMultiSession(/* multiSession= */ true)
.setUuidAndExoMediaDrmProvider(
drmConfiguration.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
}
MediaSource createdMediaSource;
switch (mimeType) {
case DemoUtil.MIME_TYPE_SS:
createdMediaSource =
new SsMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
case DemoUtil.MIME_TYPE_DASH:
createdMediaSource =
new DashMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
case DemoUtil.MIME_TYPE_HLS:
createdMediaSource =
new HlsMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
case DemoUtil.MIME_TYPE_VIDEO_MP4:
createdMediaSource =
new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
break;
default:
throw new IllegalArgumentException("mimeType is unsupported: " + mimeType);
}
return createdMediaSource;
}
} }

View File

@ -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>

View File

@ -13,17 +13,10 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView" android:id="@+id/textView"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="20sp"
android:gravity="center" android:gravity="center"
android:textSize="20sp"
android:text="@string/cast_context_error"/> android:text="@string/cast_context_error"/>
</LinearLayout>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -20,8 +20,12 @@
<string name="media_route_menu_title">Cast</string> <string name="media_route_menu_title">Cast</string>
<string name="sample_list_dialog_title">Add samples</string> <string name="add_samples">Add samples</string>
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string> <string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
<string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
</resources> </resources>

View File

@ -1,4 +0,0 @@
# IMA demo application #
This folder contains a demo application that showcases ExoPlayer integration
with the IMA SDK.

View File

@ -1,58 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.imademo;
import android.app.Activity;
import android.os.Bundle;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ui.PlayerView;
/**
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
* {@link PlayerManager}, which this class instantiates.
*/
public final class MainActivity extends Activity {
private PlayerView playerView;
private PlayerManager player;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
playerView = findViewById(R.id.player_view);
player = new PlayerManager(this);
}
@Override
public void onResume() {
super.onResume();
player.init(this, playerView);
}
@Override
public void onPause() {
super.onPause();
player.reset();
}
@Override
public void onDestroy() {
player.release();
super.onDestroy();
}
}

View File

@ -1,134 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.imademo;
import android.content.Context;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */
/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory {
private final ImaAdsLoader adsLoader;
private final DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
private long contentPosition;
public PlayerManager(Context context) {
String adTag = context.getString(R.string.ad_tag_url);
adsLoader = new ImaAdsLoader(context, Uri.parse(adTag));
dataSourceFactory =
new DefaultDataSourceFactory(
context, Util.getUserAgent(context, context.getString(R.string.application_name)));
}
public void init(Context context, PlayerView playerView) {
// Create a default track selector.
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
// Create a player instance.
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
// Bind the player to the view.
playerView.setPlayer(player);
// This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url);
MediaSource contentMediaSource = buildMediaSource(Uri.parse(contentUrl));
// Compose the content media source into a new AdsMediaSource with both ads and content.
MediaSource mediaSourceWithAds =
new AdsMediaSource(
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
playerView.getOverlayFrameLayout());
// Prepare the player with the source.
player.seekTo(contentPosition);
player.prepare(mediaSourceWithAds);
player.setPlayWhenReady(true);
}
public void reset() {
if (player != null) {
contentPosition = player.getContentPosition();
player.release();
player = null;
}
}
public void release() {
if (player != null) {
player.release();
player = null;
}
adsLoader.release();
}
// AdsMediaSource.MediaSourceFactory implementation.
@Override
public MediaSource createMediaSource(Uri uri) {
return buildMediaSource(uri);
}
@Override
public int[] getSupportedTypes() {
// IMA does not support Smooth Streaming ads.
return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER};
}
// Internal methods.
private MediaSource buildMediaSource(Uri uri) {
@ContentType int type = Util.inferContentType(uri);
switch (type) {
case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
case C.TYPE_OTHER:
return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
}
}

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="application_name">Exo IMA Demo</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>
</resources>

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -26,8 +25,8 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
@ -45,8 +44,9 @@ android {
} }
lintOptions { lintOptions {
// The demo app does not have translations. // The demo app isn't indexed, doesn't have translations, and has a
disable 'MissingTranslation' // banner for AndroidTV that's only in xhdpi density.
disable 'GoogleAppIndexingWarning','MissingTranslation','IconDensities'
} }
flavorDimensions "extensions" flavorDimensions "extensions"
@ -62,12 +62,15 @@ android {
} }
dependencies { dependencies {
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'com.google.android.material:material:1.0.0'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
withExtensionsImplementation project(path: modulePrefix + 'extension-av1')
withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg')
withExtensionsImplementation project(path: modulePrefix + 'extension-flac') withExtensionsImplementation project(path: modulePrefix + 'extension-flac')
withExtensionsImplementation project(path: modulePrefix + 'extension-ima') withExtensionsImplementation project(path: modulePrefix + 'extension-ima')

View File

@ -15,6 +15,7 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.demo"> package="com.google.android.exoplayer2.demo">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
@ -33,11 +34,14 @@
android:banner="@drawable/ic_banner" android:banner="@drawable/ic_banner"
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:name="com.google.android.exoplayer2.demo.DemoApplication"> android:requestLegacyExternalStorage="true"
android:name="com.google.android.exoplayer2.demo.DemoApplication"
tools:ignore="UnusedAttribute">
<activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity" <activity android:name="com.google.android.exoplayer2.demo.SampleChooserActivity"
android:configChanges="keyboardHidden" android:configChanges="keyboardHidden"
android:label="@string/application_name"> android:label="@string/application_name"
android:theme="@style/Theme.AppCompat">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@ -208,6 +208,13 @@
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd", "uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure and Clear SD & HD (cenc,MP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"drm_session_for_clear_types": ["audio", "video"]
} }
] ]
}, },
@ -330,11 +337,11 @@
"samples": [ "samples": [
{ {
"name": "Super speed", "name": "Super speed",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism" "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
}, },
{ {
"name": "Super speed (PlayReady)", "name": "Super speed (PlayReady)",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", "uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
"drm_scheme": "playready" "drm_scheme": "playready"
} }
] ]
@ -376,44 +383,52 @@
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"name": "Apple AAC 10s", "name": "Apple 10s (AAC)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
}, },
{ {
"name": "Apple TS 10s", "name": "Apple 10s (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
}, },
{ {
"name": "Android screens (Matroska)", "name": "Android screens (MKV)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
}, },
{ {
"name": "Screens 360P (WebM,VP9,No Audio)", "name": "Screens 360p video (WebM,VP9)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-vp9-360.webm"
}, },
{ {
"name": "Screens 480p (FMP4,H264,No Audio)", "name": "Screens 480p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4"
}, },
{ {
"name": "Screens 1080p (FMP4,H264, No Audio)", "name": "Screens 1080p video (FMP4,H264)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
}, },
{ {
"name": "Screens (FMP4,AAC Audio)", "name": "Screens audio (FMP4)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
}, },
{ {
"name": "Google Play (MP3 Audio)", "name": "Google Play (MP3)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3" "uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
}, },
{ {
"name": "Google Play (Ogg/Vorbis Audio)", "name": "Google Play (Ogg/Vorbis)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg" "uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
}, },
{ {
"name": "Big Buck Bunny (FLV Video)", "name": "Google Play (FLAC)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/flac/play.flac"
},
{
"name": "Big Buck Bunny video (FLV)",
"uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0" "uri": "https://vod.leasewebcdn.com/bbb.flv?ri=1024&rs=150&start=0"
},
{
"name": "Big Buck Bunny 480p video (MP4,AV1)",
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
} }
] ]
}, },
@ -447,23 +462,27 @@
}, },
{ {
"name": "Clear -> Enc -> Clear -> Enc -> Enc", "name": "Clear -> Enc -> Clear -> Enc -> Enc",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
"playlist": [ "playlist": [
{ {
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"uri": "https://html5demos.com/assets/dizzy.mp4" "uri": "https://html5demos.com/assets/dizzy.mp4"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd" "uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
} }
] ]
} }
@ -578,5 +597,24 @@
"spherical_stereo_mode": "top_bottom" "spherical_stereo_mode": "top_bottom"
} }
] ]
},
{
"name": "Subtitles",
"samples": [
{
"name": "TTML",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "en"
},
{
"name": "SSA/ASS position & alignment",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ssa/test-subs-position.ass",
"subtitle_mime_type": "text/x-ssa",
"subtitle_language": "en"
}
]
} }
] ]

View File

@ -18,34 +18,41 @@ package com.google.android.exoplayer2.demo;
import android.app.Application; import android.app.Application;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.FileDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException;
/** /**
* Placeholder application to facilitate overriding Application methods for debugging and testing. * Placeholder application to facilitate overriding Application methods for debugging and testing.
*/ */
public class DemoApplication extends Application { public class DemoApplication extends Application {
private static final String TAG = "DemoApplication";
private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
protected String userAgent; protected String userAgent;
private DatabaseProvider databaseProvider;
private File downloadDirectory; private File downloadDirectory;
private Cache downloadCache; private Cache downloadCache;
private DownloadManager downloadManager; private DownloadManager downloadManager;
@ -82,7 +89,8 @@ public class DemoApplication extends Application {
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(this, extensionRendererMode); return new DefaultRenderersFactory(/* context= */ this)
.setExtensionRendererMode(extensionRendererMode);
} }
public DownloadManager getDownloadManager() { public DownloadManager getDownloadManager() {
@ -95,33 +103,51 @@ public class DemoApplication extends Application {
return downloadTracker; return downloadTracker;
} }
protected synchronized Cache getDownloadCache() {
if (downloadCache == null) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY);
downloadCache =
new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider());
}
return downloadCache;
}
private synchronized void initDownloadManager() { private synchronized void initDownloadManager() {
if (downloadManager == null) { if (downloadManager == null) {
DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider());
upgradeActionFile(
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
upgradeActionFile(
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
DownloaderConstructorHelper downloaderConstructorHelper = DownloaderConstructorHelper downloaderConstructorHelper =
new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager = downloadManager =
new DownloadManager( new DownloadManager(
this, this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE),
new DefaultDownloaderFactory(downloaderConstructorHelper),
MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
DownloadManager.DEFAULT_REQUIREMENTS);
downloadTracker = downloadTracker =
new DownloadTracker( new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
/* context= */ this,
buildDataSourceFactory(),
new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE));
downloadManager.addListener(downloadTracker);
} }
} }
private synchronized Cache getDownloadCache() { private void upgradeActionFile(
if (downloadCache == null) { String fileName, DefaultDownloadIndex downloadIndex, boolean addNewDownloadsAsCompleted) {
File downloadContentDirectory = new File(getDownloadDirectory(), DOWNLOAD_CONTENT_DIRECTORY); try {
downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor()); ActionFileUpgradeUtil.upgradeAndDelete(
new File(getDownloadDirectory(), fileName),
/* downloadIdProvider= */ null,
downloadIndex,
/* deleteOnFailure= */ true,
addNewDownloadsAsCompleted);
} catch (IOException e) {
Log.e(TAG, "Failed to upgrade action file: " + fileName, e);
} }
return downloadCache; }
private DatabaseProvider getDatabaseProvider() {
if (databaseProvider == null) {
databaseProvider = new ExoDatabaseProvider(this);
}
return databaseProvider;
} }
private File getDownloadDirectory() { private File getDownloadDirectory() {
@ -134,12 +160,12 @@ public class DemoApplication extends Application {
return downloadDirectory; return downloadDirectory;
} }
private static CacheDataSourceFactory buildReadOnlyCacheDataSource( protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
DefaultDataSourceFactory upstreamFactory, Cache cache) { DataSource.Factory upstreamFactory, Cache cache) {
return new CacheDataSourceFactory( return new CacheDataSourceFactory(
cache, cache,
upstreamFactory, upstreamFactory,
new FileDataSourceFactory(), new FileDataSource.Factory(),
/* cacheWriteDataSinkFactory= */ null, /* cacheWriteDataSinkFactory= */ null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
/* eventListener= */ null); /* eventListener= */ null);

View File

@ -16,13 +16,14 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.app.Notification; import android.app.Notification;
import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.DownloadState;
import com.google.android.exoplayer2.scheduler.PlatformScheduler; import com.google.android.exoplayer2.scheduler.PlatformScheduler;
import com.google.android.exoplayer2.ui.DownloadNotificationUtil; import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.NotificationUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.List;
/** A service for downloading media. */ /** A service for downloading media. */
public class DemoDownloadService extends DownloadService { public class DemoDownloadService extends DownloadService {
@ -33,15 +34,24 @@ public class DemoDownloadService extends DownloadService {
private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
private DownloadNotificationHelper notificationHelper;
public DemoDownloadService() { public DemoDownloadService() {
super( super(
FOREGROUND_NOTIFICATION_ID, FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
CHANNEL_ID, CHANNEL_ID,
R.string.exo_download_notification_channel_name); R.string.exo_download_notification_channel_name,
/* channelDescriptionResourceId= */ 0);
nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1;
} }
@Override
public void onCreate() {
super.onCreate();
notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID);
}
@Override @Override
protected DownloadManager getDownloadManager() { protected DownloadManager getDownloadManager() {
return ((DemoApplication) getApplication()).getDownloadManager(); return ((DemoApplication) getApplication()).getDownloadManager();
@ -53,35 +63,26 @@ public class DemoDownloadService extends DownloadService {
} }
@Override @Override
protected Notification getForegroundNotification(DownloadState[] downloadStates) { protected Notification getForegroundNotification(List<Download> downloads) {
return DownloadNotificationUtil.buildProgressNotification( return notificationHelper.buildProgressNotification(
/* context= */ this, R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloads);
R.drawable.ic_download,
CHANNEL_ID,
/* contentIntent= */ null,
/* message= */ null,
downloadStates);
} }
@Override @Override
protected void onDownloadStateChanged(DownloadState downloadState) { protected void onDownloadChanged(Download download) {
Notification notification = null; Notification notification;
if (downloadState.state == DownloadState.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
notification = notification =
DownloadNotificationUtil.buildDownloadCompletedNotification( notificationHelper.buildDownloadCompletedNotification(
/* context= */ this,
R.drawable.ic_download_done, R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(downloadState.customMetadata)); Util.fromUtf8Bytes(download.request.data));
} else if (downloadState.state == DownloadState.STATE_FAILED) { } else if (download.state == Download.STATE_FAILED) {
notification = notification =
DownloadNotificationUtil.buildDownloadFailedNotification( notificationHelper.buildDownloadFailedNotification(
/* context= */ this,
R.drawable.ic_download_done, R.drawable.ic_download_done,
CHANNEL_ID,
/* contentIntent= */ null, /* contentIntent= */ null,
Util.fromUtf8Bytes(downloadState.customMetadata)); Util.fromUtf8Bytes(download.request.data));
} else { } else {
return; return;
} }

View File

@ -15,61 +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.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.Nullable;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.offline.ActionFile; import com.google.android.exoplayer2.offline.Download;
import com.google.android.exoplayer2.offline.DownloadAction; 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.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.offline.DownloadState;
import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.scheduler.Requirements;
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.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;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.ui.DefaultTrackNameProvider;
import com.google.android.exoplayer2.ui.TrackNameProvider;
import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
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.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 {
@ -82,23 +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;
public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) { @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
public DownloadTracker(
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
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();
} }
public void addListener(Listener listener) { public void addListener(Listener listener) {
@ -110,283 +81,194 @@ 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( public void toggleDownload(
Activity activity, FragmentManager fragmentManager,
String name, String name,
Uri uri, Uri uri,
String extension, String extension,
RenderersFactory renderersFactory) { RenderersFactory renderersFactory) {
if (isDownloaded(uri)) { Download download = downloads.get(uri);
DownloadAction removeAction = if (download != null) {
getDownloadHelper(uri, extension, renderersFactory).getRemoveAction(); DownloadService.sendRemoveDownload(
startServiceWithAction(removeAction); context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
} else { } else {
new StartDownloadDialogHelper( if (startDownloadDialogHelper != null) {
activity, getDownloadHelper(uri, extension, renderersFactory), name); startDownloadDialogHelper.release();
}
}
// DownloadManager.Listener
@Override
public void onInitialized(DownloadManager downloadManager) {
// Do nothing.
}
@Override
public void onDownloadStateChanged(DownloadManager downloadManager, DownloadState downloadState) {
if (downloadState.state == DownloadState.STATE_REMOVED
|| downloadState.state == DownloadState.STATE_FAILED) {
// A download has been removed, or has failed. Stop tracking it.
if (trackedDownloadStates.remove(downloadState.uri) != null) {
handleTrackedDownloadStatesChanged();
} }
startDownloadDialogHelper =
new StartDownloadDialogHelper(
fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
} }
} }
@Override private void loadDownloads() {
public void onIdle(DownloadManager downloadManager) { try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) {
// Do nothing. while (loadedDownloads.moveToNext()) {
} Download download = loadedDownloads.getDownload();
downloads.put(download.request.uri, download);
@Override
public void onRequirementsStateChanged(
DownloadManager downloadManager,
Requirements requirements,
@Requirements.RequirementFlags int notMetRequirements) {
// Do nothing.
}
// Internal methods
private void loadTrackedActions() {
try {
DownloadAction[] allActions = actionFile.load();
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) {
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, RenderersFactory renderersFactory) { Uri uri, String extension, RenderersFactory renderersFactory) {
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, renderersFactory); return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_SS: case C.TYPE_SS:
return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory); return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory); 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);
} }
} }
@SuppressWarnings("UngroupedOverloads") private class DownloadManagerListener implements DownloadManager.Listener {
@Override
public void onDownloadChanged(DownloadManager downloadManager, Download download) {
downloads.put(download.request.uri, download);
for (Listener listener : listeners) {
listener.onDownloadsChanged();
}
}
@Override
public void onDownloadRemoved(DownloadManager downloadManager, Download download) {
downloads.remove(download.request.uri);
for (Listener listener : listeners) {
listener.onDownloadsChanged();
}
}
}
private final class StartDownloadDialogHelper private final class StartDownloadDialogHelper
implements DownloadHelper.Callback, implements DownloadHelper.Callback,
DialogInterface.OnClickListener, DialogInterface.OnClickListener,
View.OnClickListener, DialogInterface.OnDismissListener {
TrackSelectionView.DialogCallback {
private final DownloadHelper<?> downloadHelper; private final FragmentManager fragmentManager;
private final DownloadHelper downloadHelper;
private final String name; private final String name;
private final LayoutInflater dialogInflater;
private final AlertDialog dialog;
private final LinearLayout selectionList;
private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo; private MappedTrackInfo mappedTrackInfo;
private DefaultTrackSelector.Parameters parameters;
private StartDownloadDialogHelper( public StartDownloadDialogHelper(
Activity activity, DownloadHelper<?> downloadHelper, String name) { FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
this.fragmentManager = fragmentManager;
this.downloadHelper = downloadHelper; this.downloadHelper = downloadHelper;
this.name = name; this.name = name;
AlertDialog.Builder builder =
new AlertDialog.Builder(activity)
.setTitle(R.string.download_preparing)
.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.
dialogInflater = LayoutInflater.from(builder.getContext());
selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null);
builder.setView(selectionList);
dialog = builder.create();
dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
parameters = DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS;
downloadHelper.prepare(this); downloadHelper.prepare(this);
} }
public void release() {
downloadHelper.release();
if (trackSelectionDialog != null) {
trackSelectionDialog.dismiss();
}
}
// DownloadHelper.Callback implementation. // DownloadHelper.Callback implementation.
@Override @Override
public void onPrepared(DownloadHelper<?> helper) { public void onPrepared(DownloadHelper helper) {
if (helper.getPeriodCount() < 1) { if (helper.getPeriodCount() == 0) {
onPrepareError(downloadHelper, new IOException("Content is empty.")); Log.d(TAG, "No periods found. Downloading entire stream.");
startDownload();
downloadHelper.release();
return; return;
} }
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
updateSelectionList(); if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
dialog.setTitle(R.string.exo_download_description); Log.d(TAG, "No dialog content. Downloading entire stream.");
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); startDownload();
} downloadHelper.release();
return;
@Override
public void onPrepareError(DownloadHelper<?> helper, IOException e) {
Toast.makeText(
context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Failed to start download", e);
dialog.cancel();
}
// View.OnClickListener implementation.
@Override
public void onClick(View v) {
Integer rendererIndex = (Integer) v.getTag();
String dialogTitle = getTrackTypeString(mappedTrackInfo.getRendererType(rendererIndex));
Pair<AlertDialog, TrackSelectionView> dialogPair =
TrackSelectionView.getDialog(
dialog.getContext(),
dialogTitle,
mappedTrackInfo,
rendererIndex,
parameters,
/* callback= */ this);
dialogPair.second.setShowDisableOption(true);
dialogPair.second.setAllowAdaptiveSelections(false);
dialogPair.first.show();
}
// TrackSelectionView.DialogCallback implementation.
@Override
public void onTracksSelected(DefaultTrackSelector.Parameters parameters) {
for (int i = 0; i < downloadHelper.getPeriodCount(); i++) {
downloadHelper.replaceTrackSelections(/* periodIndex= */ i, parameters);
} }
this.parameters = parameters; trackSelectionDialog =
updateSelectionList(); TrackSelectionDialog.createForMappedTrackInfoAndParameters(
/* titleId= */ R.string.exo_download_description,
mappedTrackInfo,
trackSelectorParameters,
/* allowAdaptiveSelections =*/ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
/* onDismissListener= */ this);
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
}
@Override
public void onPrepareError(DownloadHelper helper, IOException e) {
Toast.makeText(context, R.string.download_start_error, Toast.LENGTH_LONG).show();
Log.e(
TAG,
e instanceof DownloadHelper.LiveContentUnsupportedException
? "Downloading live content unsupported"
: "Failed to start download",
e);
} }
// DialogInterface.OnClickListener implementation. // DialogInterface.OnClickListener implementation.
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
DownloadAction downloadAction = downloadHelper.getDownloadAction(Util.getUtf8Bytes(name)); for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
startDownload(downloadAction); downloadHelper.clearTrackSelections(periodIndex);
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex,
/* rendererIndex= */ i,
trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
}
}
}
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. // Internal methods.
private void updateSelectionList() { private void startDownload() {
selectionList.removeAllViews(); startDownload(buildDownloadRequest());
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i);
if (trackGroupArray.length == 0) {
continue;
}
String trackTypeString =
getTrackTypeString(mappedTrackInfo.getRendererType(/* rendererIndex= */ i));
if (trackTypeString == null) {
return;
}
String trackSelectionsString = getTrackSelectionString(/* rendererIndex= */ i);
View view = dialogInflater.inflate(R.layout.download_track_item, selectionList, false);
TextView trackTitleView = view.findViewById(R.id.track_title);
TextView trackDescView = view.findViewById(R.id.track_desc);
ImageButton editButton = view.findViewById(R.id.edit_button);
trackTitleView.setText(trackTypeString);
trackDescView.setText(trackSelectionsString);
editButton.setTag(i);
editButton.setOnClickListener(this);
selectionList.addView(view);
}
} }
private String getTrackSelectionString(int rendererIndex) { private void startDownload(DownloadRequest downloadRequest) {
List<TrackSelection> trackSelections = DownloadService.sendAddDownload(
downloadHelper.getTrackSelections(/* periodIndex= */ 0, rendererIndex); context, DemoDownloadService.class, downloadRequest, /* foreground= */ false);
String selectedTracks = "";
Resources resources = selectionList.getResources();
for (int i = 0; i < trackSelections.size(); i++) {
TrackSelection selection = trackSelections.get(i);
for (int j = 0; j < selection.length(); j++) {
String trackName = trackNameProvider.getTrackName(selection.getFormat(j));
if (i == 0 && j == 0) {
selectedTracks = trackName;
} else {
selectedTracks = resources.getString(R.string.exo_item_list, selectedTracks, trackName);
}
}
}
return selectedTracks.isEmpty()
? resources.getString(R.string.exo_track_selection_none)
: selectedTracks;
} }
@Nullable private DownloadRequest buildDownloadRequest() {
private String getTrackTypeString(int trackType) { return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
Resources resources = selectionList.getResources();
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:
return null;
}
} }
} }
} }

View File

@ -15,44 +15,47 @@
*/ */
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.media.MediaDrm;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
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.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format;
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.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.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
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.MediaDrmCallback;
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.StreamKey; import com.google.android.exoplayer2.offline.DownloadHelper;
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.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.MergingMediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
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;
@ -68,8 +71,7 @@ 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.SphericalGLSurfaceView;
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;
import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.ErrorMessageProvider;
@ -79,42 +81,54 @@ import java.lang.reflect.Constructor;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.List; import java.util.List;
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 IS_LIVE_EXTRA = "is_live";
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_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types";
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 TUNNELING_EXTRA = "tunneling";
public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
// 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";
@ -128,12 +142,13 @@ public class PlayerActivity extends Activity
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 List<MediaSource> mediaSources;
private MediaSource mediaSource;
private DefaultTrackSelector trackSelector; private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters; private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper; private DebugTextViewHelper debugViewHelper;
@ -147,13 +162,13 @@ public class PlayerActivity extends Activity
private AdsLoader adsLoader; private AdsLoader adsLoader;
private Uri loadedAdTagUri; private Uri loadedAdTagUri;
private ViewGroup adUiViewGroup;
// 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);
} }
@ -164,10 +179,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);
@ -186,7 +201,7 @@ public class PlayerActivity extends Activity
finish(); finish();
return; return;
} }
((SphericalSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode); ((SphericalGLSurfaceView) playerView.getVideoSurfaceView()).setDefaultStereoMode(stereoMode);
} }
if (savedInstanceState != null) { if (savedInstanceState != null) {
@ -195,13 +210,20 @@ 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(); DefaultTrackSelector.ParametersBuilder builder =
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false);
if (Util.SDK_INT >= 21 && tunneling) {
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
}
trackSelectorParameters = builder.build();
clearStartPosition(); clearStartPosition();
} }
} }
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
releasePlayer(); releasePlayer();
releaseAdsLoader(); releaseAdsLoader();
clearStartPosition(); clearStartPosition();
@ -276,6 +298,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);
@ -296,23 +319,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();
}
} }
} }
@ -320,7 +335,7 @@ public class PlayerActivity extends Activity
@Override @Override
public void preparePlayback() { public void preparePlayback() {
initializePlayer(); player.retry();
} }
// PlaybackControlView.VisibilityListener implementation // PlaybackControlView.VisibilityListener implementation
@ -335,69 +350,10 @@ 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(); mediaSources = createTopLevelMediaSources(intent);
Uri[] uris; if (mediaSources.isEmpty()) {
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; 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;
if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA);
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;
}
}
TrackSelection.Factory trackSelectionFactory; TrackSelection.Factory trackSelectionFactory;
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA); String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) { if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
@ -415,13 +371,14 @@ public class PlayerActivity extends Activity
RenderersFactory renderersFactory = RenderersFactory renderersFactory =
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders); ((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
trackSelector = new DefaultTrackSelector(trackSelectionFactory); trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
trackSelector.setParameters(trackSelectorParameters); trackSelector.setParameters(trackSelectorParameters);
lastSeenTrackGroupArray = null; lastSeenTrackGroupArray = null;
player = player =
ExoPlayerFactory.newSimpleInstance( new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
/* context= */ this, renderersFactory, trackSelector, drmSessionManager); .setTrackSelector(trackSelector)
.build();
player.addListener(new PlayerEventListener()); player.addListener(new PlayerEventListener());
player.setPlayWhenReady(startAutoPlay); player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector)); player.addAnalyticsListener(new EventLogger(trackSelector));
@ -429,74 +386,151 @@ 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;
if (haveStartPosition) { if (haveStartPosition) {
player.seekTo(startWindow, startPosition); player.seekTo(startWindow, startPosition);
} }
player.prepare(mediaSource, !haveStartPosition, false); player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition);
updateButtonVisibilities(); player.prepare();
updateButtonVisibility();
} }
private MediaSource buildMediaSource(Uri uri) { private List<MediaSource> createTopLevelMediaSources(Intent intent) {
return buildMediaSource(uri, null); String action = intent.getAction();
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
showToast(getString(R.string.unexpected_intent_action, action));
finish();
return null;
}
Sample intentAsSample = Sample.createFromIntent(intent);
UriSample[] samples =
intentAsSample instanceof Sample.PlaylistSample
? ((Sample.PlaylistSample) intentAsSample).children
: new UriSample[] {(UriSample) intentAsSample};
boolean seenAdsTagUri = false;
for (UriSample sample : samples) {
seenAdsTagUri |= sample.adTagUri != null;
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
showToast(R.string.error_cleartext_not_permitted);
return null;
}
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
// The player will be reinitialized if the permission is granted.
return null;
}
}
List<MediaSource> mediaSources = new ArrayList<>();
for (UriSample sample : samples) {
MediaSource mediaSource = createLeafMediaSource(sample);
Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo;
if (subtitleInfo != null) {
Format subtitleFormat =
Format.createTextSampleFormat(
/* id= */ null,
subtitleInfo.mimeType,
C.SELECTION_FLAG_DEFAULT,
subtitleInfo.language);
MediaSource subtitleMediaSource =
new SingleSampleMediaSource.Factory(dataSourceFactory)
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource);
}
mediaSources.add(mediaSource);
}
if (seenAdsTagUri && mediaSources.size() == 1) {
Uri adTagUri = samples[0].adTagUri;
if (!adTagUri.equals(loadedAdTagUri)) {
releaseAdsLoader();
loadedAdTagUri = adTagUri;
}
MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri);
if (adsMediaSource != null) {
mediaSources.set(0, adsMediaSource);
} else {
showToast(R.string.ima_not_loaded);
}
} else if (seenAdsTagUri && mediaSources.size() > 1) {
showToast(R.string.unsupported_ads_in_concatenation);
releaseAdsLoader();
} else {
releaseAdsLoader();
}
return mediaSources;
} }
@SuppressWarnings("unchecked") private MediaSource createLeafMediaSource(UriSample parameters) {
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { Sample.DrmInfo drmInfo = parameters.drmInfo;
@ContentType int type = Util.inferContentType(uri, overrideExtension); int errorStringId = R.string.error_drm_unknown;
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri); DrmSessionManager<ExoMediaCrypto> drmSessionManager = null;
if (drmInfo == null) {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
} else if (Util.SDK_INT < 18) {
errorStringId = R.string.error_drm_unsupported_before_api_18;
} else if (!MediaDrm.isCryptoSchemeSupported(drmInfo.drmScheme)) {
errorStringId = R.string.error_drm_unsupported_scheme;
} else {
MediaDrmCallback mediaDrmCallback =
createMediaDrmCallback(drmInfo.drmLicenseUrl, drmInfo.drmKeyRequestProperties);
drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(drmInfo.drmScheme, FrameworkMediaDrm.DEFAULT_PROVIDER)
.setMultiSession(drmInfo.drmMultiSession)
.setUseDrmSessionsForClearContent(drmInfo.drmSessionForClearTypes)
.build(mediaDrmCallback);
}
if (drmSessionManager == null) {
showToast(errorStringId);
finish();
return null;
}
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<?> 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)
.setStreamKeys(offlineStreamKeys) .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_SS: case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory) return new SsMediaSource.Factory(dataSourceFactory)
.setStreamKeys(offlineStreamKeys) .setDrmSessionManager(drmSessionManager)
.createMediaSource(uri); .createMediaSource(uri);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory) return new HlsMediaSource.Factory(dataSourceFactory)
.setStreamKeys(offlineStreamKeys) .setDrmSessionManager(drmSessionManager)
.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) { private HttpMediaDrmCallback createMediaDrmCallback(
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri); String licenseUrl, String[] keyRequestPropertiesArray) {
}
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
throws UnsupportedDrmException {
HttpDataSource.Factory licenseDataSourceFactory = HttpDataSource.Factory licenseDataSourceFactory =
((DemoApplication) getApplication()).buildHttpDataSourceFactory(); ((DemoApplication) getApplication()).buildHttpDataSourceFactory();
HttpMediaDrmCallback drmCallback = HttpMediaDrmCallback drmCallback =
@ -507,9 +541,7 @@ public class PlayerActivity extends Activity
keyRequestPropertiesArray[i + 1]); keyRequestPropertiesArray[i + 1]);
} }
} }
releaseMediaDrm(); return drmCallback;
mediaDrm = FrameworkMediaDrm.newInstance(uuid);
return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession);
} }
private void releasePlayer() { private void releasePlayer() {
@ -520,20 +552,12 @@ public class PlayerActivity extends Activity
debugViewHelper = null; debugViewHelper = null;
player.release(); player.release();
player = null; player = null;
mediaSource = null; mediaSources = null;
trackSelector = null; trackSelector = null;
} }
if (adsLoader != null) { if (adsLoader != null) {
adsLoader.setPlayer(null); adsLoader.setPlayer(null);
} }
releaseMediaDrm();
}
private void releaseMediaDrm() {
if (mediaDrm != null) {
mediaDrm.release();
mediaDrm = null;
}
} }
private void releaseAdsLoader() { private void releaseAdsLoader() {
@ -571,7 +595,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 {
@ -585,16 +610,23 @@ 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);
} }
adsLoader.setPlayer(player); MediaSourceFactory adMediaSourceFactory =
AdsMediaSource.MediaSourceFactory adMediaSourceFactory = new MediaSourceFactory() {
new AdsMediaSource.MediaSourceFactory() {
private DrmSessionManager<?> drmSessionManager =
DrmSessionManager.getDummyDrmSessionManager();
@Override
public MediaSourceFactory setDrmSessionManager(DrmSessionManager<?> drmSessionManager) {
this.drmSessionManager = drmSessionManager;
return this;
}
@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);
} }
@Override @Override
@ -602,7 +634,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;
@ -613,41 +645,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() {
@ -679,20 +679,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
@ -701,8 +692,7 @@ public class PlayerActivity extends Activity
clearStartPosition(); clearStartPosition();
initializePlayer(); initializePlayer();
} else { } else {
updateStartPosition(); updateButtonVisibility();
updateButtonVisibilities();
showControls(); showControls();
} }
} }
@ -710,7 +700,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) {
@ -739,7 +729,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) {
@ -754,12 +744,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);
} }
} }
} }

View File

@ -0,0 +1,277 @@
/*
* 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.DRM_SESSION_FOR_CLEAR_TYPES_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA;
import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_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.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.HashSet;
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);
boolean isLive =
intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false);
Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
return new UriSample(
/* name= */ null,
uri,
extension,
isLive,
DrmInfo.createFromIntent(intent, extrasKeySuffix),
adTagUri,
/* sphericalStereoMode= */ null,
SubtitleInfo.createFromIntent(intent, extrasKeySuffix));
}
public final Uri uri;
public final String extension;
public final boolean isLive;
public final DrmInfo drmInfo;
public final Uri adTagUri;
@Nullable public final String sphericalStereoMode;
@Nullable SubtitleInfo subtitleInfo;
public UriSample(
String name,
Uri uri,
String extension,
boolean isLive,
DrmInfo drmInfo,
Uri adTagUri,
@Nullable String sphericalStereoMode,
@Nullable SubtitleInfo subtitleInfo) {
super(name);
this.uri = uri;
this.extension = extension;
this.isLive = isLive;
this.drmInfo = drmInfo;
this.adTagUri = adTagUri;
this.sphericalStereoMode = sphericalStereoMode;
this.subtitleInfo = subtitleInfo;
}
@Override
public void addToIntent(Intent intent) {
intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive);
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());
intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive);
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);
}
if (subtitleInfo != null) {
subtitleInfo.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);
String[] drmSessionForClearTypesExtra =
intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix);
int[] drmSessionForClearTypes = toTrackTypeArray(drmSessionForClearTypesExtra);
boolean drmMultiSession =
intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
return new DrmInfo(
drmScheme,
drmLicenseUrl,
keyRequestPropertiesArray,
drmSessionForClearTypes,
drmMultiSession);
}
public final UUID drmScheme;
public final String drmLicenseUrl;
public final String[] drmKeyRequestProperties;
public final int[] drmSessionForClearTypes;
public final boolean drmMultiSession;
public DrmInfo(
UUID drmScheme,
String drmLicenseUrl,
String[] drmKeyRequestProperties,
int[] drmSessionForClearTypes,
boolean drmMultiSession) {
this.drmScheme = drmScheme;
this.drmLicenseUrl = drmLicenseUrl;
this.drmKeyRequestProperties = drmKeyRequestProperties;
this.drmSessionForClearTypes = drmSessionForClearTypes;
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);
ArrayList<String> typeStrings = new ArrayList<>();
for (int type : drmSessionForClearTypes) {
// Only audio and video are supported.
typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video");
}
intent.putExtra(
DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0]));
intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
}
}
public static final class SubtitleInfo {
@Nullable
public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) {
if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
return null;
}
return new SubtitleInfo(
Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix),
intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix));
}
public final Uri uri;
public final String mimeType;
@Nullable public final String language;
public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) {
this.uri = Assertions.checkNotNull(uri);
this.mimeType = Assertions.checkNotNull(mimeType);
this.language = language;
}
public void addToIntent(Intent intent, String extrasKeySuffix) {
intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString());
intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType);
intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language);
}
}
public static int[] toTrackTypeArray(@Nullable String[] trackTypeStringsArray) {
if (trackTypeStringsArray == null) {
return new int[0];
}
HashSet<Integer> trackTypes = new HashSet<>();
for (String trackTypeString : trackTypeStringsArray) {
switch (Util.toLowerInvariant(trackTypeString)) {
case "audio":
trackTypes.add(C.TRACK_TYPE_AUDIO);
break;
case "video":
trackTypes.add(C.TRACK_TYPE_VIDEO);
break;
default:
throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
}
}
return Util.toArray(new ArrayList<>(trackTypes));
}
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);
}

View File

@ -15,14 +15,12 @@
*/ */
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 android.util.JsonReader; import android.util.JsonReader;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -36,8 +34,13 @@ import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.demo.Sample.DrmInfo;
import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSourceInputStream;
@ -55,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";
@ -65,6 +68,7 @@ public class SampleChooserActivity extends Activity
private SampleAdapter sampleAdapter; private SampleAdapter sampleAdapter;
private MenuItem preferExtensionDecodersMenuItem; private MenuItem preferExtensionDecodersMenuItem;
private MenuItem randomAbrMenuItem; private MenuItem randomAbrMenuItem;
private MenuItem tunnelingMenuItem;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -122,6 +126,10 @@ public class SampleChooserActivity extends Activity
preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders); preferExtensionDecodersMenuItem = menu.findItem(R.id.prefer_extension_decoders);
preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers); preferExtensionDecodersMenuItem.setVisible(useExtensionRenderers);
randomAbrMenuItem = menu.findItem(R.id.random_abr); randomAbrMenuItem = menu.findItem(R.id.random_abr);
tunnelingMenuItem = menu.findItem(R.id.tunneling);
if (Util.SDK_INT < 21) {
tunnelingMenuItem.setEnabled(false);
}
return true; return true;
} }
@ -161,13 +169,18 @@ 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));
isNonNullAndChecked(randomAbrMenuItem) String abrAlgorithm =
? PlayerActivity.ABR_ALGORITHM_RANDOM isNonNullAndChecked(randomAbrMenuItem)
: PlayerActivity.ABR_ALGORITHM_DEFAULT)); ? PlayerActivity.ABR_ALGORITHM_RANDOM
: PlayerActivity.ABR_ALGORITHM_DEFAULT;
intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
sample.addToIntent(intent);
startActivity(intent);
return true; return true;
} }
@ -182,7 +195,11 @@ public class SampleChooserActivity extends Activity
((DemoApplication) getApplication()) ((DemoApplication) getApplication())
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem)); .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload( downloadTracker.toggleDownload(
this, sample.name, uriSample.uri, uriSample.extension, renderersFactory); getSupportFragmentManager(),
sample.name,
uriSample.uri,
uriSample.extension,
renderersFactory);
} }
} }
@ -194,6 +211,9 @@ public class SampleChooserActivity extends Activity
if (uriSample.drmInfo != null) { if (uriSample.drmInfo != null) {
return R.string.download_drm_unsupported; return R.string.download_drm_unsupported;
} }
if (uriSample.isLive) {
return R.string.download_live_unsupported;
}
if (uriSample.adTagUri != null) { if (uriSample.adTagUri != null) {
return R.string.download_ads_unsupported; return R.string.download_ads_unsupported;
} }
@ -283,13 +303,19 @@ public class SampleChooserActivity extends Activity
String sampleName = null; String sampleName = null;
Uri uri = null; Uri uri = null;
String extension = null; String extension = null;
boolean isLive = false;
String drmScheme = null; String drmScheme = null;
String drmLicenseUrl = null; String drmLicenseUrl = null;
String[] drmKeyRequestProperties = null; String[] drmKeyRequestProperties = null;
String[] drmSessionForClearTypes = null;
boolean drmMultiSession = false; boolean drmMultiSession = false;
ArrayList<UriSample> playlistSamples = null; ArrayList<UriSample> playlistSamples = null;
String adTagUri = null; String adTagUri = null;
String sphericalStereoMode = null; String sphericalStereoMode = null;
List<Sample.SubtitleInfo> subtitleInfos = new ArrayList<>();
Uri subtitleUri = null;
String subtitleMimeType = null;
String subtitleLanguage = null;
reader.beginObject(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
@ -305,17 +331,15 @@ 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 "is_live":
isLive = reader.nextBoolean();
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()) {
@ -325,6 +349,15 @@ public class SampleChooserActivity extends Activity
reader.endObject(); reader.endObject();
drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
break; break;
case "drm_session_for_clear_types":
ArrayList<String> drmSessionForClearTypesList = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
drmSessionForClearTypesList.add(reader.nextString());
}
reader.endArray();
drmSessionForClearTypes = drmSessionForClearTypesList.toArray(new String[0]);
break;
case "drm_multi_session": case "drm_multi_session":
drmMultiSession = reader.nextBoolean(); drmMultiSession = reader.nextBoolean();
break; break;
@ -333,7 +366,7 @@ public class SampleChooserActivity extends Activity
playlistSamples = new ArrayList<>(); playlistSamples = new ArrayList<>();
reader.beginArray(); reader.beginArray();
while (reader.hasNext()) { while (reader.hasNext()) {
playlistSamples.add((UriSample) readEntry(reader, true)); playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true));
} }
reader.endArray(); reader.endArray();
break; break;
@ -345,6 +378,15 @@ public class SampleChooserActivity extends Activity
!insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode"); !insidePlaylist, "Invalid attribute on nested item: spherical_stereo_mode");
sphericalStereoMode = reader.nextString(); sphericalStereoMode = reader.nextString();
break; break;
case "subtitle_uri":
subtitleUri = Uri.parse(reader.nextString());
break;
case "subtitle_mime_type":
subtitleMimeType = reader.nextString();
break;
case "subtitle_language":
subtitleLanguage = reader.nextString();
break;
default: default:
throw new ParserException("Unsupported attribute name: " + name); throw new ParserException("Unsupported attribute name: " + name);
} }
@ -353,18 +395,33 @@ 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,
Sample.toTrackTypeArray(drmSessionForClearTypes),
drmMultiSession);
Sample.SubtitleInfo subtitleInfo =
subtitleUri == null
? null
: new Sample.SubtitleInfo(
subtitleUri,
Assertions.checkNotNull(
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
subtitleLanguage);
if (playlistSamples != null) { if (playlistSamples != null) {
UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]); UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray); return new PlaylistSample(sampleName, playlistSamplesArray);
} else { } else {
return new UriSample( return new UriSample(
sampleName, sampleName,
drmInfo,
uri, uri,
extension, extension,
adTagUri, isLive,
sphericalStereoMode); drmInfo,
adTagUri != null ? Uri.parse(adTagUri) : null,
sphericalStereoMode,
subtitleInfo);
} }
} }
@ -476,7 +533,7 @@ public class SampleChooserActivity extends Activity
ImageButton downloadButton = view.findViewById(R.id.download_button); ImageButton downloadButton = view.findViewById(R.id.download_button);
downloadButton.setTag(sample); downloadButton.setTag(sample);
downloadButton.setColorFilter( downloadButton.setColorFilter(
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFFEEEEEE); canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
downloadButton.setImageResource( downloadButton.setImageResource(
isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download); isDownloaded ? R.drawable.ic_download_done : R.drawable.ic_download);
} }
@ -493,116 +550,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);
}
}
} }

View File

@ -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 android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
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 com.google.android.material.tabs.TabLayout;
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;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

View File

@ -1,53 +0,0 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/track_title"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="4dp"/>
<TextView
android:id="@+id/track_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="4dp"/>
</LinearLayout>
<ImageButton
android:id="@+id/edit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:contentDescription="@string/download_edit_track"
android:src="@drawable/ic_edit"/>
</LinearLayout>

View File

@ -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>

View 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>

View File

@ -13,13 +13,18 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/prefer_extension_decoders" <item android:id="@+id/prefer_extension_decoders"
android:title="@string/prefer_extension_decoders" android:title="@string/prefer_extension_decoders"
android:showAsAction="never" android:checkable="true"
android:checkable="true"/> app:showAsAction="never"/>
<item android:id="@+id/random_abr" <item android:id="@+id/random_abr"
android:title="@string/random_abr" android:title="@string/random_abr"
android:showAsAction="never" android:checkable="true"
android:checkable="true"/> app:showAsAction="never"/>
<item android:id="@+id/tunneling"
android:title="@string/tunneling"
android:checkable="true"
app:showAsAction="never"/>
</menu> </menu>

View File

@ -17,6 +17,8 @@
<string name="application_name">ExoPlayer</string> <string name="application_name">ExoPlayer</string>
<string name="track_selection_title">Select tracks</string>
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string> <string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string> <string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
@ -27,7 +29,7 @@
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string> <string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string> <string name="error_drm_unsupported_before_api_18">Protected content not supported on API levels below 18</string>
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string> <string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
@ -51,9 +53,7 @@
<string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string> <string name="ima_not_loaded">Playing sample without ads, as the IMA extension was not loaded</string>
<string name="download_edit_track">Edit selection</string> <string name="unsupported_ads_in_concatenation">Playing sample without ads, as ads are not supported in concatenations</string>
<string name="download_preparing">Preparing download…</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
@ -63,10 +63,14 @@
<string name="download_scheme_unsupported">This demo app only supports downloading http streams</string> <string name="download_scheme_unsupported">This demo app only supports downloading http streams</string>
<string name="download_live_unsupported">This demo app does not support downloading live content</string>
<string name="download_ads_unsupported">IMA does not support offline ads</string> <string name="download_ads_unsupported">IMA does not support offline ads</string>
<string name="prefer_extension_decoders">Prefer extension decoders</string> <string name="prefer_extension_decoders">Prefer extension decoders</string>
<string name="random_abr">Enable random ABR</string> <string name="random_abr">Enable random ABR</string>
<string name="tunneling">Request multimedia tunneling</string>
</resources> </resources>

View File

@ -15,13 +15,16 @@
--> -->
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PlayerTheme" parent="android:Theme.Holo"> <style name="TrackSelectionDialogThemeOverlay" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:windowNoTitle">true</item> <item name="windowNoTitle">false</item>
</style>
<style name="PlayerTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
</style> </style>
<style name="PlayerTheme.Spherical"> <style name="PlayerTheme.Spherical">
<item name="surface_type">spherical_view</item> <item name="surface_type">spherical_gl_surface_view</item>
</style> </style>
</resources> </resources>

21
demos/surface/README.md Normal file
View File

@ -0,0 +1,21 @@
# ExoPlayer SurfaceControl demo
This app demonstrates how to use the [SurfaceControl][] API to redirect video
output from ExoPlayer between different views or off-screen. `SurfaceControl`
is new in Android 10, so the app requires `minSdkVersion` 29.
The app layout has a grid of `SurfaceViews`. Initially video is output to one
of the views. Tap a `SurfaceView` to move video output to it. You can also tap
the buttons at the top of the activity to move video output off-screen, to a
full-screen `SurfaceView` or to a new activity.
When using `SurfaceControl`, the `MediaCodec` always has the same surface
attached to it, which can be freely 'reparented' to any `SurfaceView` (or
off-screen) without any interruptions to playback. This works better than
calling `MediaCodec.setOutputSurface` to change the output surface of the codec
because `MediaCodec` does not re-render its last frame when that method is
called, and because you can move output off-screen easily (`setOutputSurface`
can't take a `null` surface, so the player has to use a `DummySurface`, which
doesn't handle protected output on all devices).
[SurfaceControl]: https://developer.android.com/reference/android/view/SurfaceControl

View File

@ -1,4 +1,4 @@
// Copyright (C) 2017 The Android Open Source Project // Copyright (C) 2019 The Android Open Source Project
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -16,7 +16,6 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -26,8 +25,8 @@ android {
defaultConfig { defaultConfig {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion 16 minSdkVersion 29
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
} }
buildTypes { buildTypes {
@ -36,13 +35,10 @@ android {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt') proguardFiles getDefaultProguardFile('proguard-android.txt')
} }
debug {
jniDebuggable = true
}
} }
lintOptions { lintOptions {
// The demo app does not have translations. // This demo app does not have translations.
disable 'MissingTranslation' disable 'MissingTranslation'
} }
} }
@ -51,10 +47,5 @@ dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-smoothstreaming')
implementation project(modulePrefix + 'extension-ima')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project <!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,24 +14,28 @@
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.imademo"> package="com.google.android.exoplayer2.surfacedemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-sdk/> <uses-sdk/>
<uses-permission android:name="android.permission.INTERNET"/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher" <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
android:largeHeap="true" android:allowBackup="false"> <application
android:allowBackup="false"
<activity android:name="com.google.android.exoplayer2.imademo.MainActivity" android:icon="@mipmap/ic_launcher"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode" android:label="@string/application_name">
android:label="@string/application_name" <activity android:name=".MainActivity">
android:theme="@style/PlayerTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.surfacedemo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,282 @@
/*
* 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.surfacedemo;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.ui.PlayerControlView;
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.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.UUID;
/** Activity that demonstrates use of {@link SurfaceControl} with ExoPlayer. */
public final class MainActivity extends Activity {
private static final String DEFAULT_MEDIA_URI =
"https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv";
private static final String SURFACE_CONTROL_NAME = "surfacedemo";
private static final String ACTION_VIEW = "com.google.android.exoplayer.surfacedemo.action.VIEW";
private static final String EXTENSION_EXTRA = "extension";
private static final String DRM_SCHEME_EXTRA = "drm_scheme";
private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
private static final String OWNER_EXTRA = "owner";
private boolean isOwner;
@Nullable private PlayerControlView playerControlView;
@Nullable private SurfaceView fullScreenView;
@Nullable private SurfaceView nonFullScreenView;
@Nullable private SurfaceView currentOutputView;
@Nullable private static SimpleExoPlayer player;
@Nullable private static SurfaceControl surfaceControl;
@Nullable private static Surface videoSurface;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
playerControlView = findViewById(R.id.player_control_view);
fullScreenView = findViewById(R.id.full_screen_view);
fullScreenView.setOnClickListener(
v -> {
setCurrentOutputView(nonFullScreenView);
Assertions.checkNotNull(fullScreenView).setVisibility(View.GONE);
});
attachSurfaceListener(fullScreenView);
isOwner = getIntent().getBooleanExtra(OWNER_EXTRA, /* defaultValue= */ true);
GridLayout gridLayout = findViewById(R.id.grid_layout);
for (int i = 0; i < 9; i++) {
View view;
if (i == 0) {
Button button = new Button(/* context= */ this);
view = button;
button.setText(getString(R.string.no_output_label));
button.setOnClickListener(v -> reparent(/* surfaceView= */ null));
} else if (i == 1) {
Button button = new Button(/* context= */ this);
view = button;
button.setText(getString(R.string.full_screen_label));
button.setOnClickListener(
v -> {
setCurrentOutputView(fullScreenView);
Assertions.checkNotNull(fullScreenView).setVisibility(View.VISIBLE);
});
} else if (i == 2) {
Button button = new Button(/* context= */ this);
view = button;
button.setText(getString(R.string.new_activity_label));
button.setOnClickListener(
v ->
startActivity(
new Intent(MainActivity.this, MainActivity.class)
.putExtra(OWNER_EXTRA, /* value= */ false)));
} else {
SurfaceView surfaceView = new SurfaceView(this);
view = surfaceView;
attachSurfaceListener(surfaceView);
surfaceView.setOnClickListener(
v -> {
setCurrentOutputView(surfaceView);
nonFullScreenView = surfaceView;
});
if (nonFullScreenView == null) {
nonFullScreenView = surfaceView;
}
}
gridLayout.addView(view);
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
layoutParams.width = 0;
layoutParams.height = 0;
layoutParams.columnSpec = GridLayout.spec(i % 3, 1f);
layoutParams.rowSpec = GridLayout.spec(i / 3, 1f);
layoutParams.bottomMargin = 10;
layoutParams.leftMargin = 10;
layoutParams.topMargin = 10;
layoutParams.rightMargin = 10;
view.setLayoutParams(layoutParams);
}
}
@Override
public void onResume() {
super.onResume();
if (isOwner && player == null) {
initializePlayer();
}
setCurrentOutputView(nonFullScreenView);
PlayerControlView playerControlView = Assertions.checkNotNull(this.playerControlView);
playerControlView.setPlayer(player);
playerControlView.show();
}
@Override
public void onPause() {
super.onPause();
Assertions.checkNotNull(playerControlView).setPlayer(null);
}
@Override
public void onDestroy() {
super.onDestroy();
if (isOwner && isFinishing()) {
if (surfaceControl != null) {
surfaceControl.release();
surfaceControl = null;
}
if (videoSurface != null) {
videoSurface.release();
videoSurface = null;
}
if (player != null) {
player.release();
player = null;
}
}
}
private void initializePlayer() {
Intent intent = getIntent();
String action = intent.getAction();
Uri uri =
ACTION_VIEW.equals(action)
? Assertions.checkNotNull(intent.getData())
: Uri.parse(DEFAULT_MEDIA_URI);
String userAgent = Util.getUserAgent(this, getString(R.string.application_name));
DrmSessionManager<ExoMediaCrypto> drmSessionManager;
if (intent.hasExtra(DRM_SCHEME_EXTRA)) {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback);
} else {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
}
DataSource.Factory dataSourceFactory =
new DefaultDataSourceFactory(
this, Util.getUserAgent(this, getString(R.string.application_name)));
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
} else if (type == C.TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManager(drmSessionManager)
.createMediaSource(uri);
} else {
throw new IllegalStateException();
}
SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build();
player.prepare(mediaSource);
player.play();
player.setRepeatMode(Player.REPEAT_MODE_ALL);
surfaceControl =
new SurfaceControl.Builder()
.setName(SURFACE_CONTROL_NAME)
.setBufferSize(/* width= */ 0, /* height= */ 0)
.build();
videoSurface = new Surface(surfaceControl);
player.setVideoSurface(videoSurface);
MainActivity.player = player;
}
private void setCurrentOutputView(@Nullable SurfaceView surfaceView) {
currentOutputView = surfaceView;
if (surfaceView != null && surfaceView.getHolder().getSurface() != null) {
reparent(surfaceView);
}
}
private void attachSurfaceListener(SurfaceView surfaceView) {
surfaceView
.getHolder()
.addCallback(
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
if (surfaceView == currentOutputView) {
reparent(surfaceView);
}
}
@Override
public void surfaceChanged(
SurfaceHolder surfaceHolder, int format, int width, int height) {}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {}
});
}
private static void reparent(@Nullable SurfaceView surfaceView) {
SurfaceControl surfaceControl = Assertions.checkNotNull(MainActivity.surfaceControl);
if (surfaceView == null) {
new SurfaceControl.Transaction()
.reparent(surfaceControl, /* newParent= */ null)
.setBufferSize(surfaceControl, /* w= */ 0, /* h= */ 0)
.setVisibility(surfaceControl, /* visible= */ false)
.apply();
} else {
SurfaceControl newParentSurfaceControl = surfaceView.getSurfaceControl();
new SurfaceControl.Transaction()
.reparent(surfaceControl, newParentSurfaceControl)
.setBufferSize(surfaceControl, surfaceView.getWidth(), surfaceView.getHeight())
.setVisibility(surfaceControl, /* visible= */ true)
.apply();
}
}
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<GridLayout
android:id="@+id/grid_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:columnCount="3"/>
<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/player_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:show_timeout="0"/>
</LinearLayout>
<SurfaceView
android:id="@+id/full_screen_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
</FrameLayout>

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project <!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -13,11 +13,11 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources>
<style name="PlayerTheme" parent="android:Theme.Holo"> <string name="application_name">ExoPlayer SurfaceControl demo</string>
<item name="android:windowNoTitle">true</item> <string name="no_output_label">No output</string>
<item name="android:windowBackground">@android:color/black</item> <string name="full_screen_label">Full screen</string>
</style> <string name="new_activity_label">New activity</string>
</resources> </resources>

134
extensions/av1/README.md Normal file
View File

@ -0,0 +1,134 @@
# ExoPlayer AV1 extension #
The AV1 extension provides `Libgav1VideoRenderer`, which uses libgav1 native
library to decode AV1 videos.
## License note ##
Please note that whilst the code in this repository is licensed under
[Apache 2.0][], using this extension also requires building and including one or
more external libraries as described below. These are licensed separately.
[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE
## Build instructions (Linux, macOS) ##
To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's
[top level README][].
In addition, it's necessary to fetch cpu_features library and libgav1 with its
dependencies as follows:
* Set the following environment variables:
```
cd "<path to exoplayer checkout>"
EXOPLAYER_ROOT="$(pwd)"
AV1_EXT_PATH="${EXOPLAYER_ROOT}/extensions/av1/src/main"
```
* Fetch cpu_features library:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://github.com/google/cpu_features
```
* Fetch libgav1:
```
cd "${AV1_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/codecs/libgav1 libgav1
```
* Fetch Abseil:
```
cd "${AV1_EXT_PATH}/jni/libgav1" && \
git clone https://github.com/abseil/abseil-cpp.git third_party/abseil-cpp
```
* [Install CMake][].
Having followed these steps, gradle will build the extension automatically when
run on the command line or via Android Studio, using [CMake][] and [Ninja][]
to configure and build libgav1 and the extension's [JNI wrapper library][].
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[Install CMake]: https://developer.android.com/studio/projects/install-ndk
[CMake]: https://cmake.org/
[Ninja]: https://ninja-build.org
[JNI wrapper library]: https://github.com/google/ExoPlayer/blob/release-v2/extensions/av1/src/main/jni/gav1_jni.cc
## Build instructions (Windows) ##
We do not provide support for building this extension on Windows, however it
should be possible to follow the Linux instructions in [Windows PowerShell][].
[Windows PowerShell]: https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell
## Using the extension ##
Once you've followed the instructions above to check out, build and depend on
the extension, the next step is to tell ExoPlayer to use `Libgav1VideoRenderer`.
How you do this depends on which player API you're using:
* If you're passing a `DefaultRenderersFactory` to `SimpleExoPlayer.Builder`,
you can enable using the extension by setting the `extensionRendererMode`
parameter of the `DefaultRenderersFactory` constructor to
`EXTENSION_RENDERER_MODE_ON`. This will use `Libgav1VideoRenderer` for
playback if `MediaCodecVideoRenderer` doesn't support decoding the input AV1
stream. Pass `EXTENSION_RENDERER_MODE_PREFER` to give `Libgav1VideoRenderer`
priority over `MediaCodecVideoRenderer`.
* If you've subclassed `DefaultRenderersFactory`, add a `Libvgav1VideoRenderer`
to the output list in `buildVideoRenderers`. ExoPlayer will use the first
`Renderer` in the list that supports the input media format.
* If you've implemented your own `RenderersFactory`, return a
`Libgav1VideoRenderer` instance from `createRenderers`. ExoPlayer will use the
first `Renderer` in the returned array that supports the input media format.
* If you're using `ExoPlayer.Builder`, pass a `Libgav1VideoRenderer` in the
array of `Renderer`s. ExoPlayer will use the first `Renderer` in the list that
supports the input media format.
Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation.
You need to make sure you are passing a `Libgav1VideoRenderer` to the player and
then you need to implement your own logic to use the renderer for a given track.
## Using the extension in the demo application ##
To try out playback using the extension in the [demo application][], see
[enabling extension decoders][].
[demo application]: https://exoplayer.dev/demo-application.html
[enabling extension decoders]: https://exoplayer.dev/demo-application.html#enabling-extension-decoders
## Rendering options ##
There are two possibilities for rendering the output `Libgav1VideoRenderer`
gets from the libgav1 decoder:
* GL rendering using GL shader for color space conversion
* If you are using `SimpleExoPlayer` with `PlayerView`, enable this option by
setting `surface_type` of `PlayerView` to be
`video_decoder_gl_surface_view`.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message
of type `C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER` with an instance of
`VideoDecoderOutputBufferRenderer` as its object.
* Native rendering using `ANativeWindow`
* If you are using `SimpleExoPlayer` with `PlayerView`, this option is enabled
by default.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a message of
type `C.MSG_SET_SURFACE` with an instance of `SurfaceView` as its object.
Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred
## Links ##
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.av1.*`
belong to this module.
[Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -0,0 +1,73 @@
// 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.library'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
externalNativeBuild {
cmake {
// Debug CMake build type causes video frames to drop,
// so native library should always use Release build type.
arguments "-DCMAKE_BUILD_TYPE=Release"
targets "gav1JNI"
}
}
}
// This option resolves the problem of finding libgav1JNI.so
// on multiple paths. The first one found is picked.
packagingOptions {
pickFirst 'lib/arm64-v8a/libgav1JNI.so'
pickFirst 'lib/armeabi-v7a/libgav1JNI.so'
pickFirst 'lib/x86/libgav1JNI.so'
pickFirst 'lib/x86_64/libgav1JNI.so'
}
sourceSets.main {
// As native JNI library build is invoked from gradle, this is
// not needed. However, it exposes the built library and keeps
// consistency with the other extensions.
jniLibs.srcDir 'src/main/libs'
}
}
// Configure the native build only if libgav1 is present, to avoid gradle sync
// failures if libgav1 hasn't been checked out according to the README and CMake
// isn't installed.
if (project.file('src/main/jni/libgav1').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
android.externalNativeBuild.cmake.version = '3.7.1+'
}
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
}
ext {
javadocTitle = 'AV1 extension'
}
apply from: '../../javadoc_library.gradle'

View File

@ -0,0 +1,7 @@
# Proguard rules specific to the AV1 extension.
# This prevents the names of native methods from being obfuscated.
-keepclasseswithmembernames class * {
native <methods>;
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project <!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,4 +14,4 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="com.google.android.exoplayer2.testutil"/> <manifest package="com.google.android.exoplayer2.ext.av1"/>

View File

@ -0,0 +1,234 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.av1;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import java.nio.ByteBuffer;
/** Gav1 decoder. */
/* package */ final class Gav1Decoder
extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, Gav1DecoderException> {
// LINT.IfChange
private static final int GAV1_ERROR = 0;
private static final int GAV1_OK = 1;
private static final int GAV1_DECODE_ONLY = 2;
// LINT.ThenChange(../../../../../../../jni/gav1_jni.cc)
private final long gav1DecoderContext;
@C.VideoOutputMode private volatile int outputMode;
/**
* Creates a Gav1Decoder.
*
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
* @param initialInputBufferSize The initial size of each input buffer, in bytes.
* @param threads Number of threads libgav1 will use to decode.
* @throws Gav1DecoderException Thrown if an exception occurs when initializing the decoder.
*/
public Gav1Decoder(
int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, int threads)
throws Gav1DecoderException {
super(
new VideoDecoderInputBuffer[numInputBuffers],
new VideoDecoderOutputBuffer[numOutputBuffers]);
if (!Gav1Library.isAvailable()) {
throw new Gav1DecoderException("Failed to load decoder native library.");
}
gav1DecoderContext = gav1Init(threads);
if (gav1DecoderContext == GAV1_ERROR || gav1CheckError(gav1DecoderContext) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Failed to initialize decoder. Error: " + gav1GetErrorMessage(gav1DecoderContext));
}
setInitialInputBufferSize(initialInputBufferSize);
}
@Override
public String getName() {
return "libgav1";
}
/**
* Sets the output mode for frames rendered by the decoder.
*
* @param outputMode The output mode.
*/
public void setOutputMode(@C.VideoOutputMode int outputMode) {
this.outputMode = outputMode;
}
@Override
protected VideoDecoderInputBuffer createInputBuffer() {
return new VideoDecoderInputBuffer();
}
@Override
protected VideoDecoderOutputBuffer createOutputBuffer() {
return new VideoDecoderOutputBuffer(this::releaseOutputBuffer);
}
@Nullable
@Override
protected Gav1DecoderException decode(
VideoDecoderInputBuffer inputBuffer, VideoDecoderOutputBuffer outputBuffer, boolean reset) {
ByteBuffer inputData = Util.castNonNull(inputBuffer.data);
int inputSize = inputData.limit();
if (gav1Decode(gav1DecoderContext, inputData, inputSize) == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1Decode error: " + gav1GetErrorMessage(gav1DecoderContext));
}
boolean decodeOnly = inputBuffer.isDecodeOnly();
if (!decodeOnly) {
outputBuffer.init(inputBuffer.timeUs, outputMode, /* supplementalData= */ null);
}
// We need to dequeue the decoded frame from the decoder even when the input data is
// decode-only.
int getFrameResult = gav1GetFrame(gav1DecoderContext, outputBuffer, decodeOnly);
if (getFrameResult == GAV1_ERROR) {
return new Gav1DecoderException(
"gav1GetFrame error: " + gav1GetErrorMessage(gav1DecoderContext));
}
if (getFrameResult == GAV1_DECODE_ONLY) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
if (!decodeOnly) {
outputBuffer.colorInfo = inputBuffer.colorInfo;
}
return null;
}
@Override
protected Gav1DecoderException createUnexpectedDecodeException(Throwable error) {
return new Gav1DecoderException("Unexpected decode error", error);
}
@Override
public void release() {
super.release();
gav1Close(gav1DecoderContext);
}
@Override
protected void releaseOutputBuffer(VideoDecoderOutputBuffer buffer) {
// Decode only frames do not acquire a reference on the internal decoder buffer and thus do not
// require a call to gav1ReleaseFrame.
if (buffer.mode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && !buffer.isDecodeOnly()) {
gav1ReleaseFrame(gav1DecoderContext, buffer);
}
super.releaseOutputBuffer(buffer);
}
/**
* Renders output buffer to the given surface. Must only be called when in {@link
* C#VIDEO_OUTPUT_MODE_SURFACE_YUV} mode.
*
* @param outputBuffer Output buffer.
* @param surface Output surface.
* @throws Gav1DecoderException Thrown if called with invalid output mode or frame rendering
* fails.
*/
public void renderToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (outputBuffer.mode != C.VIDEO_OUTPUT_MODE_SURFACE_YUV) {
throw new Gav1DecoderException("Invalid output mode.");
}
if (gav1RenderFrame(gav1DecoderContext, surface, outputBuffer) == GAV1_ERROR) {
throw new Gav1DecoderException(
"Buffer render error: " + gav1GetErrorMessage(gav1DecoderContext));
}
}
/**
* Initializes a libgav1 decoder.
*
* @param threads Number of threads to be used by a libgav1 decoder.
* @return The address of the decoder context or {@link #GAV1_ERROR} if there was an error.
*/
private native long gav1Init(int threads);
/**
* Deallocates the decoder context.
*
* @param context Decoder context.
*/
private native void gav1Close(long context);
/**
* Decodes the encoded data passed.
*
* @param context Decoder context.
* @param encodedData Encoded data.
* @param length Length of the data buffer.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1Decode(long context, ByteBuffer encodedData, int length);
/**
* Gets the decoded frame.
*
* @param context Decoder context.
* @param outputBuffer Output buffer for the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_DECODE_ONLY} if successful but the frame
* is decode-only, {@link #GAV1_ERROR} if an error occurred.
*/
private native int gav1GetFrame(
long context, VideoDecoderOutputBuffer outputBuffer, boolean decodeOnly);
/**
* Renders the frame to the surface. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param surface Output surface.
* @param outputBuffer Output buffer with the decoded frame.
* @return {@link #GAV1_OK} if successful, {@link #GAV1_ERROR} if an error occured.
*/
private native int gav1RenderFrame(
long context, Surface surface, VideoDecoderOutputBuffer outputBuffer);
/**
* Releases the frame. Used with {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV} only.
*
* @param context Decoder context.
* @param outputBuffer Output buffer.
*/
private native void gav1ReleaseFrame(long context, VideoDecoderOutputBuffer outputBuffer);
/**
* Returns a human-readable string describing the last error encountered in the given context.
*
* @param context Decoder context.
* @return A string describing the last encountered error.
*/
private native String gav1GetErrorMessage(long context);
/**
* Returns whether an error occured.
*
* @param context Decoder context.
* @return {@link #GAV1_OK} if there was no error, {@link #GAV1_ERROR} if an error occured.
*/
private native int gav1CheckError(long context);
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.av1;
import com.google.android.exoplayer2.video.VideoDecoderException;
/** Thrown when a libgav1 decoder error occurs. */
public final class Gav1DecoderException extends VideoDecoderException {
/* package */ Gav1DecoderException(String message) {
super(message);
}
/* package */ Gav1DecoderException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.av1;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.util.LibraryLoader;
/** Configures and queries the underlying native library. */
public final class Gav1Library {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.gav1");
}
private static final LibraryLoader LOADER = new LibraryLoader("gav1JNI");
private Gav1Library() {}
/** Returns whether the underlying library is available, loading it if necessary. */
public static boolean isAvailable() {
return LOADER.isAvailable();
}
}

View File

@ -0,0 +1,197 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.av1;
import static java.lang.Runtime.getRuntime;
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer;
import com.google.android.exoplayer2.video.VideoDecoderException;
import com.google.android.exoplayer2.video.VideoDecoderInputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
/**
* Decodes and renders video using libgav1 decoder.
*
* <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
* on the playback thread:
*
* <ul>
* <li>Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
* should be the target {@link Surface}, or null.
* <li>Message with type {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER} to set the output
* buffer renderer. The message payload should be the target {@link
* VideoDecoderOutputBufferRenderer}, or null.
* </ul>
*/
public class Libgav1VideoRenderer extends SimpleDecoderVideoRenderer {
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
/* Default size based on 720p resolution video compressed by a factor of two. */
private static final int DEFAULT_INPUT_BUFFER_SIZE =
Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2;
/** The number of input buffers. */
private final int numInputBuffers;
/**
* The number of output buffers. The renderer may limit the minimum possible value due to
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
private final int numOutputBuffers;
private final int threads;
@Nullable private Gav1Decoder decoder;
/**
* Creates a Libgav1VideoRenderer.
*
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
public Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* threads= */ getRuntime().availableProcessors(),
DEFAULT_NUM_OF_INPUT_BUFFERS,
DEFAULT_NUM_OF_OUTPUT_BUFFERS);
}
/**
* Creates a Libgav1VideoRenderer.
*
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
* @param threads Number of threads libgav1 will use to decode.
* @param numInputBuffers Number of input buffers.
* @param numOutputBuffers Number of output buffers.
*/
public Libgav1VideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify,
int threads,
int numInputBuffers,
int numOutputBuffers) {
super(
allowedJoiningTimeMs,
eventHandler,
eventListener,
maxDroppedFramesToNotify,
/* drmSessionManager= */ null,
/* playClearSamplesWithoutKeys= */ false);
this.threads = threads;
this.numInputBuffers = numInputBuffers;
this.numOutputBuffers = numOutputBuffers;
}
@Override
@Capabilities
protected int supportsFormatInternal(
@Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format) {
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|| !Gav1Library.isAvailable()) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
}
if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
}
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
}
@Override
protected SimpleDecoder<
VideoDecoderInputBuffer,
? extends VideoDecoderOutputBuffer,
? extends VideoDecoderException>
createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
throws VideoDecoderException {
TraceUtil.beginSection("createGav1Decoder");
int initialInputBufferSize =
format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE;
Gav1Decoder decoder =
new Gav1Decoder(numInputBuffers, numOutputBuffers, initialInputBufferSize, threads);
this.decoder = decoder;
TraceUtil.endSection();
return decoder;
}
@Override
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws Gav1DecoderException {
if (decoder == null) {
throw new Gav1DecoderException(
"Failed to render output buffer to surface: decoder is not initialized.");
}
decoder.renderToSurface(outputBuffer, surface);
outputBuffer.release();
}
@Override
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
}
// PlayerMessage.Target implementation.
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == C.MSG_SET_SURFACE) {
setOutputSurface((Surface) message);
} else if (messageType == C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) {
setOutputBufferRenderer((VideoDecoderOutputBufferRenderer) message);
} else {
super.handleMessage(messageType, message);
}
}
}

View File

@ -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.av1;
import com.google.android.exoplayer2.util.NonNullApi;

View File

@ -0,0 +1,56 @@
# libgav1JNI requires modern CMake.
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
# libgav1JNI requires C++11.
set(CMAKE_CXX_STANDARD 11)
project(libgav1JNI C CXX)
# Devices using armeabi-v7a are not required to support
# Neon which is why Neon is disabled by default for
# armeabi-v7a build. This flag enables it.
if(${ANDROID_ABI} MATCHES "armeabi-v7a")
add_compile_options("-mfpu=neon")
add_compile_options("-fPIC")
endif()
set(libgav1_jni_root "${CMAKE_CURRENT_SOURCE_DIR}")
set(libgav1_jni_build "${CMAKE_BINARY_DIR}")
set(libgav1_jni_output_directory
${libgav1_jni_root}/../libs/${ANDROID_ABI}/)
set(libgav1_root "${libgav1_jni_root}/libgav1")
set(libgav1_build "${libgav1_jni_build}/libgav1")
set(cpu_features_root "${libgav1_jni_root}/cpu_features")
set(cpu_features_build "${libgav1_jni_build}/cpu_features")
# Build cpu_features library.
add_subdirectory("${cpu_features_root}"
"${cpu_features_build}"
EXCLUDE_FROM_ALL)
# Build libgav1.
add_subdirectory("${libgav1_root}"
"${libgav1_build}"
EXCLUDE_FROM_ALL)
# Build libgav1JNI.
add_library(gav1JNI
SHARED
gav1_jni.cc)
# Locate NDK log library.
find_library(android_log_lib log)
# Link libgav1JNI against used libraries.
target_link_libraries(gav1JNI
PRIVATE android
PRIVATE cpu_features
PRIVATE libgav1_static
PRIVATE ${android_log_lib})
# Specify output directory for libgav1JNI.
set_target_properties(gav1JNI PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
${libgav1_jni_output_directory})

View File

@ -0,0 +1,754 @@
/*
* 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.
*/
#include <android/log.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include "cpu_features_macros.h" // NOLINT
#ifdef CPU_FEATURES_ARCH_ARM
#include "cpuinfo_arm.h" // NOLINT
#endif // CPU_FEATURES_ARCH_ARM
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
#include <arm_neon.h>
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
#include <jni.h>
#include <cstring>
#include <mutex> // NOLINT
#include <new>
#include "gav1/decoder.h"
#define LOG_TAG "gav1_jni"
#define LOGE(...) \
((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \
extern "C" { \
JNIEXPORT RETURN_TYPE \
Java_com_google_android_exoplayer2_ext_av1_Gav1Decoder_##NAME( \
JNIEnv* env, jobject thiz, ##__VA_ARGS__); \
} \
JNIEXPORT RETURN_TYPE \
Java_com_google_android_exoplayer2_ext_av1_Gav1Decoder_##NAME( \
JNIEnv* env, jobject thiz, ##__VA_ARGS__)
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
return JNI_VERSION_1_6;
}
namespace {
// YUV plane indices.
const int kPlaneY = 0;
const int kPlaneU = 1;
const int kPlaneV = 2;
const int kMaxPlanes = 3;
// Android YUV format. See:
// https://developer.android.com/reference/android/graphics/ImageFormat.html#YV12.
const int kImageFormatYV12 = 0x32315659;
// LINT.IfChange
// Output modes.
const int kOutputModeYuv = 0;
const int kOutputModeSurfaceYuv = 1;
// LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/C.java)
// LINT.IfChange
const int kColorSpaceUnknown = 0;
// LINT.ThenChange(../../../../../library/core/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java)
// LINT.IfChange
// Return codes for jni methods.
const int kStatusError = 0;
const int kStatusOk = 1;
const int kStatusDecodeOnly = 2;
// LINT.ThenChange(../java/com/google/android/exoplayer2/ext/av1/Gav1Decoder.java)
// Status codes specific to the JNI wrapper code.
enum JniStatusCode {
kJniStatusOk = 0,
kJniStatusOutOfMemory = -1,
kJniStatusBufferAlreadyReleased = -2,
kJniStatusInvalidNumOfPlanes = -3,
kJniStatusBitDepth12NotSupportedWithYuv = -4,
kJniStatusHighBitDepthNotSupportedWithSurfaceYuv = -5,
kJniStatusANativeWindowError = -6,
kJniStatusBufferResizeError = -7,
kJniStatusNeonNotSupported = -8
};
const char* GetJniErrorMessage(JniStatusCode error_code) {
switch (error_code) {
case kJniStatusOutOfMemory:
return "Out of memory.";
case kJniStatusBufferAlreadyReleased:
return "JNI buffer already released.";
case kJniStatusBitDepth12NotSupportedWithYuv:
return "Bit depth 12 is not supported with YUV.";
case kJniStatusHighBitDepthNotSupportedWithSurfaceYuv:
return "High bit depth (10 or 12 bits per pixel) output format is not "
"supported with YUV surface.";
case kJniStatusInvalidNumOfPlanes:
return "Libgav1 decoded buffer has invalid number of planes.";
case kJniStatusANativeWindowError:
return "ANativeWindow error.";
case kJniStatusBufferResizeError:
return "Buffer resize failed.";
case kJniStatusNeonNotSupported:
return "Neon is not supported.";
default:
return "Unrecognized error code.";
}
}
// Manages Libgav1FrameBuffer and reference information.
class JniFrameBuffer {
public:
explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {
gav1_frame_buffer_.private_data = &id_;
}
~JniFrameBuffer() {
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
delete[] gav1_frame_buffer_.data[plane_index];
}
}
void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) {
for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes();
plane_index++) {
stride_[plane_index] = decoder_buffer.stride[plane_index];
plane_[plane_index] = decoder_buffer.plane[plane_index];
displayed_width_[plane_index] =
decoder_buffer.displayed_width[plane_index];
displayed_height_[plane_index] =
decoder_buffer.displayed_height[plane_index];
}
}
int Stride(int plane_index) const { return stride_[plane_index]; }
uint8_t* Plane(int plane_index) const { return plane_[plane_index]; }
int DisplayedWidth(int plane_index) const {
return displayed_width_[plane_index];
}
int DisplayedHeight(int plane_index) const {
return displayed_height_[plane_index];
}
// Methods maintaining reference count are not thread-safe. They must be
// called with a lock held.
void AddReference() { ++reference_count_; }
void RemoveReference() { reference_count_--; }
bool InUse() const { return reference_count_ != 0; }
const Libgav1FrameBuffer& GetGav1FrameBuffer() const {
return gav1_frame_buffer_;
}
// Attempts to reallocate data planes if the existing ones don't have enough
// capacity. Returns true if the allocation was successful or wasn't needed,
// false if the allocation failed.
bool MaybeReallocateGav1DataPlanes(int y_plane_min_size,
int uv_plane_min_size) {
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
const int min_size =
(plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size;
if (gav1_frame_buffer_.size[plane_index] >= min_size) continue;
delete[] gav1_frame_buffer_.data[plane_index];
gav1_frame_buffer_.data[plane_index] =
new (std::nothrow) uint8_t[min_size];
if (!gav1_frame_buffer_.data[plane_index]) {
gav1_frame_buffer_.size[plane_index] = 0;
return false;
}
gav1_frame_buffer_.size[plane_index] = min_size;
}
return true;
}
private:
int stride_[kMaxPlanes];
uint8_t* plane_[kMaxPlanes];
int displayed_width_[kMaxPlanes];
int displayed_height_[kMaxPlanes];
int id_;
int reference_count_;
Libgav1FrameBuffer gav1_frame_buffer_ = {};
};
// Manages frame buffers used by libgav1 decoder and ExoPlayer.
// Handles synchronization between libgav1 and ExoPlayer threads.
class JniBufferManager {
public:
~JniBufferManager() {
// This lock does not do anything since libgav1 has released all the frame
// buffers. It exists to merely be consistent with all other usage of
// |all_buffers_| and |all_buffer_count_|.
std::lock_guard<std::mutex> lock(mutex_);
while (all_buffer_count_--) {
delete all_buffers_[all_buffer_count_];
}
}
JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size,
Libgav1FrameBuffer* frame_buffer) {
std::lock_guard<std::mutex> lock(mutex_);
JniFrameBuffer* output_buffer;
if (free_buffer_count_) {
output_buffer = free_buffers_[--free_buffer_count_];
} else if (all_buffer_count_ < kMaxFrames) {
output_buffer = new (std::nothrow) JniFrameBuffer(all_buffer_count_);
if (output_buffer == nullptr) return kJniStatusOutOfMemory;
all_buffers_[all_buffer_count_++] = output_buffer;
} else {
// Maximum number of buffers is being used.
return kJniStatusOutOfMemory;
}
if (!output_buffer->MaybeReallocateGav1DataPlanes(y_plane_min_size,
uv_plane_min_size)) {
return kJniStatusOutOfMemory;
}
output_buffer->AddReference();
*frame_buffer = output_buffer->GetGav1FrameBuffer();
return kJniStatusOk;
}
JniFrameBuffer* GetBuffer(int id) const { return all_buffers_[id]; }
void AddBufferReference(int id) {
std::lock_guard<std::mutex> lock(mutex_);
all_buffers_[id]->AddReference();
}
JniStatusCode ReleaseBuffer(int id) {
std::lock_guard<std::mutex> lock(mutex_);
JniFrameBuffer* buffer = all_buffers_[id];
if (!buffer->InUse()) {
return kJniStatusBufferAlreadyReleased;
}
buffer->RemoveReference();
if (!buffer->InUse()) {
free_buffers_[free_buffer_count_++] = buffer;
}
return kJniStatusOk;
}
private:
static const int kMaxFrames = 32;
JniFrameBuffer* all_buffers_[kMaxFrames];
int all_buffer_count_ = 0;
JniFrameBuffer* free_buffers_[kMaxFrames];
int free_buffer_count_ = 0;
std::mutex mutex_;
};
struct JniContext {
~JniContext() {
if (native_window) {
ANativeWindow_release(native_window);
}
}
bool MaybeAcquireNativeWindow(JNIEnv* env, jobject new_surface) {
if (surface == new_surface) {
return true;
}
if (native_window) {
ANativeWindow_release(native_window);
}
native_window_width = 0;
native_window_height = 0;
native_window = ANativeWindow_fromSurface(env, new_surface);
if (native_window == nullptr) {
jni_status_code = kJniStatusANativeWindowError;
surface = nullptr;
return false;
}
surface = new_surface;
return true;
}
jfieldID decoder_private_field;
jfieldID output_mode_field;
jfieldID data_field;
jmethodID init_for_private_frame_method;
jmethodID init_for_yuv_frame_method;
JniBufferManager buffer_manager;
// The libgav1 decoder instance has to be deleted before |buffer_manager| is
// destructed. This will make sure that libgav1 releases all the frame
// buffers that it might be holding references to. So this has to be declared
// after |buffer_manager| since the destruction happens in reverse order of
// declaration.
libgav1::Decoder decoder;
ANativeWindow* native_window = nullptr;
jobject surface = nullptr;
int native_window_width = 0;
int native_window_height = 0;
Libgav1StatusCode libgav1_status_code = kLibgav1StatusOk;
JniStatusCode jni_status_code = kJniStatusOk;
};
int Libgav1GetFrameBuffer(void* private_data, size_t y_plane_min_size,
size_t uv_plane_min_size,
Libgav1FrameBuffer* frame_buffer) {
JniContext* const context = reinterpret_cast<JniContext*>(private_data);
context->jni_status_code = context->buffer_manager.GetBuffer(
y_plane_min_size, uv_plane_min_size, frame_buffer);
if (context->jni_status_code != kJniStatusOk) {
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
return -1;
}
return 0;
}
int Libgav1ReleaseFrameBuffer(void* private_data,
Libgav1FrameBuffer* frame_buffer) {
JniContext* const context = reinterpret_cast<JniContext*>(private_data);
const int buffer_id = *reinterpret_cast<int*>(frame_buffer->private_data);
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
if (context->jni_status_code != kJniStatusOk) {
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
return -1;
}
return 0;
}
constexpr int AlignTo16(int value) { return (value + 15) & (~15); }
void CopyPlane(const uint8_t* source, int source_stride, uint8_t* destination,
int destination_stride, int width, int height) {
while (height--) {
std::memcpy(destination, source, width);
source += source_stride;
destination += destination_stride;
}
}
void CopyFrameToDataBuffer(const libgav1::DecoderBuffer* decoder_buffer,
jbyte* data) {
for (int plane_index = kPlaneY; plane_index < decoder_buffer->NumPlanes();
plane_index++) {
const uint64_t length = decoder_buffer->stride[plane_index] *
decoder_buffer->displayed_height[plane_index];
memcpy(data, decoder_buffer->plane[plane_index], length);
data += length;
}
}
void Convert10BitFrameTo8BitDataBuffer(
const libgav1::DecoderBuffer* decoder_buffer, jbyte* data) {
for (int plane_index = kPlaneY; plane_index < decoder_buffer->NumPlanes();
plane_index++) {
int sample = 0;
const uint8_t* source = decoder_buffer->plane[plane_index];
for (int i = 0; i < decoder_buffer->displayed_height[plane_index]; i++) {
const uint16_t* source_16 = reinterpret_cast<const uint16_t*>(source);
for (int j = 0; j < decoder_buffer->displayed_width[plane_index]; j++) {
// Lightweight dither. Carryover the remainder of each 10->8 bit
// conversion to the next pixel.
sample += source_16[j];
data[j] = sample >> 2;
sample &= 3; // Remainder.
}
source += decoder_buffer->stride[plane_index];
data += decoder_buffer->stride[plane_index];
}
}
}
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
void Convert10BitFrameTo8BitDataBufferNeon(
const libgav1::DecoderBuffer* decoder_buffer, jbyte* data) {
uint32x2_t lcg_value = vdup_n_u32(random());
lcg_value = vset_lane_u32(random(), lcg_value, 1);
// LCG values recommended in "Numerical Recipes".
const uint32x2_t LCG_MULT = vdup_n_u32(1664525);
const uint32x2_t LCG_INCR = vdup_n_u32(1013904223);
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
const uint8_t* source = decoder_buffer->plane[plane_index];
for (int i = 0; i < decoder_buffer->displayed_height[plane_index]; i++) {
const uint16_t* source_16 = reinterpret_cast<const uint16_t*>(source);
uint8_t* destination = reinterpret_cast<uint8_t*>(data);
// Each read consumes 4 2-byte samples, but to reduce branches and
// random steps we unroll to 4 rounds, so each loop consumes 16
// samples.
const int j_max = decoder_buffer->displayed_width[plane_index] & ~15;
int j;
for (j = 0; j < j_max; j += 16) {
// Run a round of the RNG.
lcg_value = vmla_u32(LCG_INCR, lcg_value, LCG_MULT);
// Round 1.
// The lower two bits of this LCG parameterization are garbage,
// leaving streaks on the image. We access the upper bits of each
// 16-bit lane by shifting. (We use this both as an 8- and 16-bit
// vector, so the choice of which one to keep it as is arbitrary.)
uint8x8_t randvec =
vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_value), 8));
// We retrieve the values and shift them so that the bits we'll
// shift out (after biasing) are in the upper 8 bits of each 16-bit
// lane.
uint16x4_t values = vshl_n_u16(vld1_u16(source_16), 6);
// We add the bias bits in the lower 8 to the shifted values to get
// the final values in the upper 8 bits.
uint16x4_t added_1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
source_16 += 4;
// Round 2.
// Shifting the randvec bits left by 2 bits, as an 8-bit vector,
// should leave us with enough bias to get the needed rounding
// operation.
randvec = vshl_n_u8(randvec, 2);
// Retrieve and sum the next 4 pixels.
values = vshl_n_u16(vld1_u16(source_16), 6);
uint16x4_t added_2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
source_16 += 4;
// Reinterpret the two added vectors as 8x8, zip them together, and
// discard the lower portions.
uint8x8_t zipped =
vuzp_u8(vreinterpret_u8_u16(added_1), vreinterpret_u8_u16(added_2))
.val[1];
vst1_u8(destination, zipped);
destination += 8;
// Run it again with the next two rounds using the remaining
// entropy in randvec.
// Round 3.
randvec = vshl_n_u8(randvec, 2);
values = vshl_n_u16(vld1_u16(source_16), 6);
added_1 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
source_16 += 4;
// Round 4.
randvec = vshl_n_u8(randvec, 2);
values = vshl_n_u16(vld1_u16(source_16), 6);
added_2 = vqadd_u16(values, vreinterpret_u16_u8(randvec));
source_16 += 4;
zipped =
vuzp_u8(vreinterpret_u8_u16(added_1), vreinterpret_u8_u16(added_2))
.val[1];
vst1_u8(destination, zipped);
destination += 8;
}
uint32_t randval = 0;
// For the remaining pixels in each row - usually none, as most
// standard sizes are divisible by 32 - convert them "by hand".
for (; j < decoder_buffer->displayed_width[plane_index]; j++) {
if (!randval) randval = random();
destination[j] = (source_16[j] + (randval & 3)) >> 2;
randval >>= 2;
}
source += decoder_buffer->stride[plane_index];
data += decoder_buffer->stride[plane_index];
}
}
}
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
} // namespace
DECODER_FUNC(jlong, gav1Init, jint threads) {
JniContext* context = new (std::nothrow) JniContext();
if (context == nullptr) {
return kStatusError;
}
#ifdef CPU_FEATURES_ARCH_ARM
// Libgav1 requires NEON with arm ABIs.
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
const cpu_features::ArmFeatures arm_features =
cpu_features::GetArmInfo().features;
if (!arm_features.neon) {
context->jni_status_code = kJniStatusNeonNotSupported;
return reinterpret_cast<jlong>(context);
}
#else
context->jni_status_code = kJniStatusNeonNotSupported;
return reinterpret_cast<jlong>(context);
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
#endif // CPU_FEATURES_ARCH_ARM
libgav1::DecoderSettings settings;
settings.threads = threads;
settings.get = Libgav1GetFrameBuffer;
settings.release = Libgav1ReleaseFrameBuffer;
settings.callback_private_data = context;
context->libgav1_status_code = context->decoder.Init(&settings);
if (context->libgav1_status_code != kLibgav1StatusOk) {
return reinterpret_cast<jlong>(context);
}
// Populate JNI References.
const jclass outputBufferClass = env->FindClass(
"com/google/android/exoplayer2/video/VideoDecoderOutputBuffer");
context->decoder_private_field =
env->GetFieldID(outputBufferClass, "decoderPrivate", "I");
context->output_mode_field = env->GetFieldID(outputBufferClass, "mode", "I");
context->data_field =
env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;");
context->init_for_private_frame_method =
env->GetMethodID(outputBufferClass, "initForPrivateFrame", "(II)V");
context->init_for_yuv_frame_method =
env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z");
return reinterpret_cast<jlong>(context);
}
DECODER_FUNC(void, gav1Close, jlong jContext) {
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
delete context;
}
DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData,
jint length) {
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
const uint8_t* const buffer = reinterpret_cast<const uint8_t*>(
env->GetDirectBufferAddress(encodedData));
context->libgav1_status_code =
context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0);
if (context->libgav1_status_code != kLibgav1StatusOk) {
return kStatusError;
}
return kStatusOk;
}
DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer,
jboolean decodeOnly) {
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
const libgav1::DecoderBuffer* decoder_buffer;
context->libgav1_status_code = context->decoder.DequeueFrame(&decoder_buffer);
if (context->libgav1_status_code != kLibgav1StatusOk) {
return kStatusError;
}
if (decodeOnly || decoder_buffer == nullptr) {
// This is not an error. The input data was decode-only or no displayable
// frames are available.
return kStatusDecodeOnly;
}
const int output_mode =
env->GetIntField(jOutputBuffer, context->output_mode_field);
if (output_mode == kOutputModeYuv) {
// Resize the buffer if required. Default color conversion will be used as
// libgav1::DecoderBuffer doesn't expose color space info.
const jboolean init_result = env->CallBooleanMethod(
jOutputBuffer, context->init_for_yuv_frame_method,
decoder_buffer->displayed_width[kPlaneY],
decoder_buffer->displayed_height[kPlaneY],
decoder_buffer->stride[kPlaneY], decoder_buffer->stride[kPlaneU],
kColorSpaceUnknown);
if (env->ExceptionCheck()) {
// Exception is thrown in Java when returning from the native call.
return kStatusError;
}
if (!init_result) {
context->jni_status_code = kJniStatusBufferResizeError;
return kStatusError;
}
const jobject data_object =
env->GetObjectField(jOutputBuffer, context->data_field);
jbyte* const data =
reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(data_object));
switch (decoder_buffer->bitdepth) {
case 8:
CopyFrameToDataBuffer(decoder_buffer, data);
break;
case 10:
#ifdef CPU_FEATURES_COMPILED_ANY_ARM_NEON
Convert10BitFrameTo8BitDataBufferNeon(decoder_buffer, data);
#else
Convert10BitFrameTo8BitDataBuffer(decoder_buffer, data);
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
break;
default:
context->jni_status_code = kJniStatusBitDepth12NotSupportedWithYuv;
return kStatusError;
}
} else if (output_mode == kOutputModeSurfaceYuv) {
if (decoder_buffer->bitdepth != 8) {
context->jni_status_code =
kJniStatusHighBitDepthNotSupportedWithSurfaceYuv;
return kStatusError;
}
if (decoder_buffer->NumPlanes() > kMaxPlanes) {
context->jni_status_code = kJniStatusInvalidNumOfPlanes;
return kStatusError;
}
const int buffer_id =
*reinterpret_cast<int*>(decoder_buffer->buffer_private_data);
context->buffer_manager.AddBufferReference(buffer_id);
JniFrameBuffer* const jni_buffer =
context->buffer_manager.GetBuffer(buffer_id);
jni_buffer->SetFrameData(*decoder_buffer);
env->CallVoidMethod(jOutputBuffer, context->init_for_private_frame_method,
decoder_buffer->displayed_width[kPlaneY],
decoder_buffer->displayed_height[kPlaneY]);
if (env->ExceptionCheck()) {
// Exception is thrown in Java when returning from the native call.
return kStatusError;
}
env->SetIntField(jOutputBuffer, context->decoder_private_field, buffer_id);
}
return kStatusOk;
}
DECODER_FUNC(jint, gav1RenderFrame, jlong jContext, jobject jSurface,
jobject jOutputBuffer) {
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
const int buffer_id =
env->GetIntField(jOutputBuffer, context->decoder_private_field);
JniFrameBuffer* const jni_buffer =
context->buffer_manager.GetBuffer(buffer_id);
if (!context->MaybeAcquireNativeWindow(env, jSurface)) {
return kStatusError;
}
if (context->native_window_width != jni_buffer->DisplayedWidth(kPlaneY) ||
context->native_window_height != jni_buffer->DisplayedHeight(kPlaneY)) {
if (ANativeWindow_setBuffersGeometry(
context->native_window, jni_buffer->DisplayedWidth(kPlaneY),
jni_buffer->DisplayedHeight(kPlaneY), kImageFormatYV12)) {
context->jni_status_code = kJniStatusANativeWindowError;
return kStatusError;
}
context->native_window_width = jni_buffer->DisplayedWidth(kPlaneY);
context->native_window_height = jni_buffer->DisplayedHeight(kPlaneY);
}
ANativeWindow_Buffer native_window_buffer;
if (ANativeWindow_lock(context->native_window, &native_window_buffer,
/*inOutDirtyBounds=*/nullptr) ||
native_window_buffer.bits == nullptr) {
context->jni_status_code = kJniStatusANativeWindowError;
return kStatusError;
}
// Y plane
CopyPlane(jni_buffer->Plane(kPlaneY), jni_buffer->Stride(kPlaneY),
reinterpret_cast<uint8_t*>(native_window_buffer.bits),
native_window_buffer.stride, jni_buffer->DisplayedWidth(kPlaneY),
jni_buffer->DisplayedHeight(kPlaneY));
const int y_plane_size =
native_window_buffer.stride * native_window_buffer.height;
const int32_t native_window_buffer_uv_height =
(native_window_buffer.height + 1) / 2;
const int native_window_buffer_uv_stride =
AlignTo16(native_window_buffer.stride / 2);
// TODO(b/140606738): Handle monochrome videos.
// V plane
// Since the format for ANativeWindow is YV12, V plane is being processed
// before U plane.
const int v_plane_height = std::min(native_window_buffer_uv_height,
jni_buffer->DisplayedHeight(kPlaneV));
CopyPlane(
jni_buffer->Plane(kPlaneV), jni_buffer->Stride(kPlaneV),
reinterpret_cast<uint8_t*>(native_window_buffer.bits) + y_plane_size,
native_window_buffer_uv_stride, jni_buffer->DisplayedWidth(kPlaneV),
v_plane_height);
const int v_plane_size = v_plane_height * native_window_buffer_uv_stride;
// U plane
CopyPlane(jni_buffer->Plane(kPlaneU), jni_buffer->Stride(kPlaneU),
reinterpret_cast<uint8_t*>(native_window_buffer.bits) +
y_plane_size + v_plane_size,
native_window_buffer_uv_stride, jni_buffer->DisplayedWidth(kPlaneU),
std::min(native_window_buffer_uv_height,
jni_buffer->DisplayedHeight(kPlaneU)));
if (ANativeWindow_unlockAndPost(context->native_window)) {
context->jni_status_code = kJniStatusANativeWindowError;
return kStatusError;
}
return kStatusOk;
}
DECODER_FUNC(void, gav1ReleaseFrame, jlong jContext, jobject jOutputBuffer) {
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
const int buffer_id =
env->GetIntField(jOutputBuffer, context->decoder_private_field);
env->SetIntField(jOutputBuffer, context->decoder_private_field, -1);
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
if (context->jni_status_code != kJniStatusOk) {
LOGE("%s", GetJniErrorMessage(context->jni_status_code));
}
}
DECODER_FUNC(jstring, gav1GetErrorMessage, jlong jContext) {
if (jContext == 0) {
return env->NewStringUTF("Failed to initialize JNI context.");
}
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
if (context->libgav1_status_code != kLibgav1StatusOk) {
return env->NewStringUTF(
libgav1::GetErrorString(context->libgav1_status_code));
}
if (context->jni_status_code != kJniStatusOk) {
return env->NewStringUTF(GetJniErrorMessage(context->jni_status_code));
}
return env->NewStringUTF("None.");
}
DECODER_FUNC(jint, gav1CheckError, jlong jContext) {
JniContext* const context = reinterpret_cast<JniContext*>(jContext);
if (context->libgav1_status_code != kLibgav1StatusOk ||
context->jni_status_code != kJniStatusOk) {
return kStatusError;
}
return kStatusOk;
}
// TODO(b/139902005): Add functions for getting libgav1 version and build
// configuration once libgav1 ABI provides this information.

View File

@ -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 ##

View File

@ -16,7 +16,6 @@ apply plugin: 'com.android.library'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -24,32 +23,22 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
} }
testOptions.unitTests.includeAndroidResources = true
} }
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:16.1.2' api 'com.google.android.gms:play-services-cast-framework:17.0.0'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation 'junit:junit:' + junitVersion
testImplementation 'org.mockito:mockito-core:' + mockitoVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
// These dependencies are necessary to force the supportLibraryVersion of
// com.android.support:support-v4, com.android.support:appcompat-v7 and
// com.android.support:mediarouter-v7 to be used. Else older versions are
// used, for example via:
// com.google.android.gms:play-services-cast-framework:15.0.1
// |-- com.android.support:mediarouter-v7:26.1.0
api 'com.android.support:support-v4:' + supportLibraryVersion
api 'com.android.support:mediarouter-v7:' + supportLibraryVersion
api 'com.android.support:recyclerview-v7:' + supportLibraryVersion
} }
ext { ext {

View File

@ -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

View File

@ -16,11 +16,11 @@
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.NonNull; import androidx.annotation.Nullable;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.BasePlayer; 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.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
@ -45,16 +45,19 @@ 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;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* {@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, {@link * injected {@link CastContext}. To keep track of the session, {@link #isCastSessionAvailable()} can
* #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be * be queried and {@link SessionAvailabilityListener} can be implemented and attached to the player.
* 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
@ -64,6 +67,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
*/ */
public final class CastPlayer extends BasePlayer { public final class CastPlayer extends BasePlayer {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.cast");
}
private static final String TAG = "CastPlayer"; private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3; private static final int RENDERER_COUNT = 3;
@ -80,29 +87,29 @@ public final class CastPlayer extends BasePlayer {
private final CastTimelineTracker timelineTracker; private final CastTimelineTracker timelineTracker;
private final Timeline.Period period; private final Timeline.Period period;
private RemoteMediaClient remoteMediaClient;
// Result callbacks. // Result callbacks.
private final StatusListener statusListener; private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback; private final SeekResultCallback seekResultCallback;
// Listeners. // Listeners and notification.
private final CopyOnWriteArraySet<EventListener> listeners; private final CopyOnWriteArrayList<ListenerHolder> listeners;
private SessionAvailabilityListener sessionAvailabilityListener; private final ArrayList<ListenerNotificationTask> notificationsBatch;
private final ArrayDeque<ListenerNotificationTask> ongoingNotificationsTasks;
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state. // Internal state.
private final StateHolder<Boolean> playWhenReady;
private final StateHolder<Integer> repeatMode;
@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 currentWindowIndex; private int currentWindowIndex;
private boolean playWhenReady;
private long lastReportedPositionMs; private long lastReportedPositionMs;
private int pendingSeekCount; private int pendingSeekCount;
private int pendingSeekWindowIndex; private int pendingSeekWindowIndex;
private long pendingSeekPositionMs; private long pendingSeekPositionMs;
private boolean waitingForInitialTimeline;
/** /**
* @param castContext The context from which the cast session is obtained. * @param castContext The context from which the cast session is obtained.
@ -113,21 +120,24 @@ public final class CastPlayer extends BasePlayer {
period = new Timeline.Period(); period = new Timeline.Period();
statusListener = new StatusListener(); statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback(); seekResultCallback = new SeekResultCallback();
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArrayList<>();
notificationsBatch = new ArrayList<>();
SessionManager sessionManager = castContext.getSessionManager(); ongoingNotificationsTasks = new ArrayDeque<>();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
CastSession session = sessionManager.getCurrentCastSession();
remoteMediaClient = session != null ? session.getRemoteMediaClient() : null;
playWhenReady = new StateHolder<>(false);
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
playbackState = STATE_IDLE; playbackState = STATE_IDLE;
repeatMode = REPEAT_MODE_OFF;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY; currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET; pendingSeekPositionMs = C.TIME_UNSET;
updateInternalState();
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
CastSession session = sessionManager.getCurrentCastSession();
setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
updateInternalStateAndNotifyIfChanged();
} }
// Media Queue manipulation methods. // Media Queue manipulation methods.
@ -141,6 +151,7 @@ public final class CastPlayer extends BasePlayer {
* starts at position 0. * starts at position 0.
* @return The Cast {@code PendingResult}, or null if no session is available. * @return The Cast {@code PendingResult}, or null if no session is available.
*/ */
@Nullable
public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) { public PendingResult<MediaChannelResult> loadItem(MediaQueueItem item, long positionMs) {
return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF);
} }
@ -156,11 +167,11 @@ public final class CastPlayer extends BasePlayer {
* @param repeatMode The repeat mode for the created media queue. * @param repeatMode The repeat mode for the created media queue.
* @return The Cast {@code PendingResult}, or null if no session is available. * @return The Cast {@code PendingResult}, or null if no session is available.
*/ */
public PendingResult<MediaChannelResult> loadItems(MediaQueueItem[] items, int startIndex, @Nullable
long positionMs, @RepeatMode int repeatMode) { public PendingResult<MediaChannelResult> loadItems(
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
if (remoteMediaClient != null) { if (remoteMediaClient != null) {
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
waitingForInitialTimeline = true;
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
positionMs, null); positionMs, null);
} }
@ -173,6 +184,7 @@ public final class CastPlayer extends BasePlayer {
* @param items The items to append. * @param items The items to append.
* @return The Cast {@code PendingResult}, or null if no media queue exists. * @return The Cast {@code PendingResult}, or null if no media queue exists.
*/ */
@Nullable
public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) { public PendingResult<MediaChannelResult> addItems(MediaQueueItem... items) {
return addItems(MediaQueueItem.INVALID_ITEM_ID, items); return addItems(MediaQueueItem.INVALID_ITEM_ID, items);
} }
@ -187,6 +199,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist. * periodId} exist.
*/ */
@Nullable
public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) { public PendingResult<MediaChannelResult> addItems(int periodId, MediaQueueItem... items) {
if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID
|| currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) {
@ -204,6 +217,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist. * periodId} exist.
*/ */
@Nullable
public PendingResult<MediaChannelResult> removeItem(int periodId) { public PendingResult<MediaChannelResult> removeItem(int periodId) {
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
return remoteMediaClient.queueRemoveItem(periodId, null); return remoteMediaClient.queueRemoveItem(periodId, null);
@ -222,6 +236,7 @@ public final class CastPlayer extends BasePlayer {
* @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code
* periodId} exist. * periodId} exist.
*/ */
@Nullable
public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) { public PendingResult<MediaChannelResult> moveItem(int periodId, int newIndex) {
Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount());
if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) {
@ -239,6 +254,7 @@ public final class CastPlayer extends BasePlayer {
* @return The item that corresponds to the period with the given id, or null if no media queue or * @return The item that corresponds to the period with the given id, or null if no media queue or
* period with id {@code periodId} exist. * period with id {@code periodId} exist.
*/ */
@Nullable
public MediaQueueItem getItem(int periodId) { public MediaQueueItem getItem(int periodId) {
MediaStatus mediaStatus = getMediaStatus(); MediaStatus mediaStatus = getMediaStatus();
return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET
@ -257,9 +273,9 @@ public final class CastPlayer extends BasePlayer {
/** /**
* Sets a listener for updates on the cast session availability. * Sets a listener for updates on the cast session availability.
* *
* @param listener The {@link SessionAvailabilityListener}. * @param listener The {@link SessionAvailabilityListener}, or null to clear the listener.
*/ */
public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { public void setSessionAvailabilityListener(@Nullable SessionAvailabilityListener listener) {
sessionAvailabilityListener = listener; sessionAvailabilityListener = listener;
} }
@ -296,20 +312,33 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void addListener(EventListener listener) { public void addListener(EventListener listener) {
listeners.add(listener); listeners.addIfAbsent(new ListenerHolder(listener));
} }
@Override @Override
public void removeListener(EventListener listener) { public void removeListener(EventListener listener) {
listeners.remove(listener); for (ListenerHolder listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) {
listenerHolder.release();
listeners.remove(listenerHolder);
}
}
} }
@Override @Override
@Player.State
public int getPlaybackState() { public int getPlaybackState() {
return playbackState; return playbackState;
} }
@Override @Override
@PlaybackSuppressionReason
public int getPlaybackSuppressionReason() {
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
}
@Override
@Nullable
public ExoPlaybackException getPlaybackError() { public ExoPlaybackException getPlaybackError() {
return null; return null;
} }
@ -319,16 +348,29 @@ public final class CastPlayer extends BasePlayer {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {
return; return;
} }
if (playWhenReady) { // We update the local state and send the message to the receiver app, which will cause the
remoteMediaClient.play(); // operation to be perceived as synchronous by the user. When the operation reports a result,
} else { // the local state will be updated to reflect the state reported by the Cast SDK.
remoteMediaClient.pause(); setPlayerStateAndNotifyIfChanged(playWhenReady, playbackState);
} flushNotifications();
PendingResult<MediaChannelResult> pendingResult =
playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause();
this.playWhenReady.pendingResultCallback =
new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult mediaChannelResult) {
if (remoteMediaClient != null) {
updatePlayerStateAndNotifyIfChanged(this);
flushNotifications();
}
}
};
pendingResult.setResultCallback(this.playWhenReady.pendingResultCallback);
} }
@Override @Override
public boolean getPlayWhenReady() { public boolean getPlayWhenReady() {
return playWhenReady; return playWhenReady.value;
} }
@Override @Override
@ -347,14 +389,13 @@ public final class CastPlayer extends BasePlayer {
pendingSeekCount++; pendingSeekCount++;
pendingSeekWindowIndex = windowIndex; pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs; pendingSeekPositionMs = positionMs;
for (EventListener listener : listeners) { notificationsBatch.add(
listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); new ListenerNotificationTask(
} listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)));
} else if (pendingSeekCount == 0) { } else if (pendingSeekCount == 0) {
for (EventListener listener : listeners) { notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
listener.onSeekProcessed();
}
} }
flushNotifications();
} }
@Override @Override
@ -405,14 +446,32 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void setRepeatMode(@RepeatMode int repeatMode) { public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient != null) { if (remoteMediaClient == null) {
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null); return;
} }
// We update the local state and send the message to the receiver app, which will cause the
// operation to be perceived as synchronous by the user. When the operation reports a result,
// the local state will be updated to reflect the state reported by the Cast SDK.
setRepeatModeAndNotifyIfChanged(repeatMode);
flushNotifications();
PendingResult<MediaChannelResult> pendingResult =
remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), /* jsonObject= */ null);
this.repeatMode.pendingResultCallback =
new ResultCallback<MediaChannelResult>() {
@Override
public void onResult(MediaChannelResult mediaChannelResult) {
if (remoteMediaClient != null) {
updateRepeatModeAndNotifyIfChanged(this);
flushNotifications();
}
}
};
pendingResult.setResultCallback(this.repeatMode.pendingResultCallback);
} }
@Override @Override
@RepeatMode public int getRepeatMode() { @RepeatMode public int getRepeatMode() {
return repeatMode; return repeatMode.value;
} }
@Override @Override
@ -441,11 +500,6 @@ public final class CastPlayer extends BasePlayer {
return currentTimeline; return currentTimeline;
} }
@Override
@Nullable public Object getCurrentManifest() {
return null;
}
@Override @Override
public int getCurrentPeriodIndex() { public int getCurrentPeriodIndex() {
return getCurrentWindowIndex(); return getCurrentWindowIndex();
@ -518,52 +572,80 @@ public final class CastPlayer extends BasePlayer {
// Internal methods. // Internal methods.
public void updateInternalState() { private void updateInternalStateAndNotifyIfChanged() {
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;
} }
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
if (wasPlaying != isPlaying) {
notificationsBatch.add(
new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
}
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
updateTimelineAndNotifyIfChanged();
int playbackState = fetchPlaybackState(remoteMediaClient); int currentWindowIndex = C.INDEX_UNSET;
boolean playWhenReady = !remoteMediaClient.isPaused(); MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
if (this.playbackState != playbackState if (currentItem != null) {
|| this.playWhenReady != playWhenReady) { currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
this.playbackState = playbackState;
this.playWhenReady = playWhenReady;
for (EventListener listener : listeners) {
listener.onPlayerStateChanged(this.playWhenReady, this.playbackState);
}
} }
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient); if (currentWindowIndex == C.INDEX_UNSET) {
if (this.repeatMode != repeatMode) { // The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
this.repeatMode = repeatMode; currentWindowIndex = 0;
for (EventListener listener : listeners) {
listener.onRepeatModeChanged(repeatMode);
}
} }
int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus());
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex; this.currentWindowIndex = currentWindowIndex;
for (EventListener listener : listeners) { notificationsBatch.add(
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); new ListenerNotificationTask(
} listener ->
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)));
} }
if (updateTracksAndSelections()) { if (updateTracksAndSelectionsAndNotifyIfChanged()) {
for (EventListener listener : listeners) { notificationsBatch.add(
listener.onTracksChanged(currentTrackGroups, currentTrackSelection); new ListenerNotificationTask(
} listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)));
} }
maybeUpdateTimelineAndNotify(); flushNotifications();
} }
private void maybeUpdateTimelineAndNotify() { /**
* Updates {@link #playWhenReady} and {@link #playbackState} to match the Cast {@code
* remoteMediaClient} state, and notifies listeners of any state changes.
*
* <p>This method will only update values whose {@link StateHolder#pendingResultCallback} matches
* the given {@code resultCallback}.
*/
@RequiresNonNull("remoteMediaClient")
private void updatePlayerStateAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
boolean newPlayWhenReadyValue = playWhenReady.value;
if (playWhenReady.acceptsUpdate(resultCallback)) {
newPlayWhenReadyValue = !remoteMediaClient.isPaused();
playWhenReady.clearPendingResultCallback();
}
// We do not mask the playback state, so try setting it regardless of the playWhenReady masking.
setPlayerStateAndNotifyIfChanged(newPlayWhenReadyValue, fetchPlaybackState(remoteMediaClient));
}
@RequiresNonNull("remoteMediaClient")
private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
if (repeatMode.acceptsUpdate(resultCallback)) {
setRepeatModeAndNotifyIfChanged(fetchRepeatMode(remoteMediaClient));
repeatMode.clearPendingResultCallback();
}
}
private void updateTimelineAndNotifyIfChanged() {
if (updateTimeline()) { if (updateTimeline()) {
@Player.TimelineChangeReason int reason = waitingForInitialTimeline // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
waitingForInitialTimeline = false; notificationsBatch.add(
for (EventListener listener : listeners) { new ListenerNotificationTask(
listener.onTimelineChanged(currentTimeline, null, reason); listener ->
} listener.onTimelineChanged(
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
} }
} }
@ -574,14 +656,14 @@ public final class CastPlayer extends BasePlayer {
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);
} }
/** /** Updates the internal tracks and selection and returns whether they have changed. */
* Updates the internal tracks and selection and returns whether they have changed. private boolean updateTracksAndSelectionsAndNotifyIfChanged() {
*/
private boolean updateTracksAndSelections() {
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 false; return false;
@ -627,6 +709,25 @@ public final class CastPlayer extends BasePlayer {
return false; return false;
} }
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
if (this.repeatMode.value != repeatMode) {
this.repeatMode.value = repeatMode;
notificationsBatch.add(
new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(repeatMode)));
}
}
private void setPlayerStateAndNotifyIfChanged(
boolean playWhenReady, @Player.State int playbackState) {
if (this.playWhenReady.value != playWhenReady || this.playbackState != playbackState) {
this.playWhenReady.value = playWhenReady;
this.playbackState = playbackState;
notificationsBatch.add(
new ListenerNotificationTask(
listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)));
}
}
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) { private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
if (this.remoteMediaClient == remoteMediaClient) { if (this.remoteMediaClient == remoteMediaClient) {
// Do nothing. // Do nothing.
@ -643,7 +744,7 @@ public final class CastPlayer extends BasePlayer {
} }
remoteMediaClient.addListener(statusListener); remoteMediaClient.addListener(statusListener);
remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS);
updateInternalState(); updateInternalStateAndNotifyIfChanged();
} else { } else {
if (sessionAvailabilityListener != null) { if (sessionAvailabilityListener != null) {
sessionAvailabilityListener.onCastSessionUnavailable(); sessionAvailabilityListener.onCastSessionUnavailable();
@ -651,7 +752,8 @@ public final class CastPlayer extends BasePlayer {
} }
} }
private @Nullable MediaStatus getMediaStatus() { @Nullable
private MediaStatus getMediaStatus() {
return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null;
} }
@ -699,16 +801,6 @@ public final class CastPlayer extends BasePlayer {
} }
} }
/**
* Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If
* there is no media session, returns 0.
*/
private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) {
Integer currentItemId = mediaStatus != null
? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null;
return currentItemId != null ? currentItemId : 0;
}
private static boolean isTrackActive(long id, long[] activeTrackIds) { private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) { for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) { if (activeTrackId == id) {
@ -739,8 +831,26 @@ public final class CastPlayer extends BasePlayer {
} }
} }
private final class StatusListener implements RemoteMediaClient.Listener, private void flushNotifications() {
SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener { 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 StatusListener
implements RemoteMediaClient.Listener,
SessionManagerListener<CastSession>,
RemoteMediaClient.ProgressListener {
// RemoteMediaClient.ProgressListener implementation. // RemoteMediaClient.ProgressListener implementation.
@ -753,7 +863,7 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void onStatusUpdated() { public void onStatusUpdated() {
updateInternalState(); updateInternalStateAndNotifyIfChanged();
} }
@Override @Override
@ -761,7 +871,7 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void onQueueStatusUpdated() { public void onQueueStatusUpdated() {
maybeUpdateTimelineAndNotify(); updateTimelineAndNotifyIfChanged();
} }
@Override @Override
@ -824,12 +934,10 @@ public final class CastPlayer extends BasePlayer {
} }
// Result callbacks hooks.
private final class SeekResultCallback implements ResultCallback<MediaChannelResult> { private final class SeekResultCallback implements ResultCallback<MediaChannelResult> {
@Override @Override
public void onResult(@NonNull MediaChannelResult result) { public void onResult(MediaChannelResult result) {
int statusCode = result.getStatus().getStatusCode(); int statusCode = result.getStatus().getStatusCode();
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) { if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
Log.e(TAG, "Seek failed. Error code " + statusCode + ": " Log.e(TAG, "Seek failed. Error code " + statusCode + ": "
@ -838,11 +946,62 @@ public final class CastPlayer extends BasePlayer {
if (--pendingSeekCount == 0) { if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET; pendingSeekPositionMs = C.TIME_UNSET;
for (EventListener listener : listeners) { notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed));
listener.onSeekProcessed(); flushNotifications();
}
} }
} }
} }
/** Holds the value and the masking status of a specific part of the {@link CastPlayer} state. */
private static final class StateHolder<T> {
/** The user-facing value of a specific part of the {@link CastPlayer} state. */
public T value;
/**
* If {@link #value} is being masked, holds the result callback for the operation that triggered
* the masking. Or null if {@link #value} is not being masked.
*/
@Nullable public ResultCallback<MediaChannelResult> pendingResultCallback;
public StateHolder(T initialValue) {
value = initialValue;
}
public void clearPendingResultCallback() {
pendingResultCallback = null;
}
/**
* Returns whether this state holder accepts updates coming from the given result callback.
*
* <p>A null {@code resultCallback} means that the update is a regular receiver state update, in
* which case the update will only be accepted if {@link #value} is not being masked. If {@link
* #value} is being masked, the update will only be accepted if {@code resultCallback} is the
* same as the {@link #pendingResultCallback}.
*
* @param resultCallback A result callback. May be null if the update comes from a regular
* receiver status update.
*/
public boolean acceptsUpdate(@Nullable ResultCallback<?> resultCallback) {
return pendingResultCallback == resultCallback;
}
}
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);
}
}
}
} }

View File

@ -15,53 +15,101 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.support.annotation.Nullable; import android.util.SparseArray;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** /**
* A {@link Timeline} for Cast media queues. * A {@link Timeline} for Cast media queues.
*/ */
/* package */ final class CastTimeline extends Timeline { /* package */ final class CastTimeline extends Timeline {
/** Holds {@link Timeline} related data for a Cast media item. */
public static final class ItemData {
/** Holds no media information. */
public static final ItemData EMPTY = new ItemData();
/** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */
public final long durationUs;
/**
* The default start position of the item in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public final long defaultPositionUs;
/** Whether the item is live content, or {@code false} if unknown. */
public final boolean isLive;
private ItemData() {
this(
/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */
C.TIME_UNSET,
/* isLive= */ false);
}
/**
* Creates an instance.
*
* @param durationUs See {@link #durationsUs}.
* @param defaultPositionUs See {@link #defaultPositionUs}.
* @param isLive See {@link #isLive}.
*/
public ItemData(long durationUs, long defaultPositionUs, boolean isLive) {
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.isLive = isLive;
}
/**
* Returns a copy of this instance with the given values.
*
* @param durationUs The duration in microseconds, or {@link C#TIME_UNSET} if unknown.
* @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET}
* if unknown.
* @param isLive Whether the item is live, or {@code false} if unknown.
*/
public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) {
if (durationUs == this.durationUs
&& defaultPositionUs == this.defaultPositionUs
&& isLive == this.isLive) {
return this;
}
return new ItemData(durationUs, defaultPositionUs, isLive);
}
}
/** {@link Timeline} for a cast queue that has no items. */
public static final CastTimeline EMPTY_CAST_TIMELINE = public static final CastTimeline EMPTY_CAST_TIMELINE =
new CastTimeline(Collections.emptyList(), Collections.emptyMap()); new CastTimeline(new int[0], new SparseArray<>());
private final SparseIntArray idsToIndex; private final SparseIntArray idsToIndex;
private final int[] ids; private final int[] ids;
private final long[] durationsUs; private final long[] durationsUs;
private final long[] defaultPositionsUs; private final long[] defaultPositionsUs;
private final boolean[] isLive;
/** /**
* @param items A list of cast media queue items to represent. * Creates a Cast timeline from the given data.
* @param contentIdToDurationUsMap A map of content id to duration in microseconds. *
* @param itemIds The ids of the items in the timeline.
* @param itemIdToData Maps item ids to {@link ItemData}.
*/ */
public CastTimeline(List<MediaQueueItem> items, Map<String, Long> contentIdToDurationUsMap) { public CastTimeline(int[] itemIds, SparseArray<ItemData> itemIdToData) {
int itemCount = items.size(); int itemCount = itemIds.length;
int index = 0;
idsToIndex = new SparseIntArray(itemCount); idsToIndex = new SparseIntArray(itemCount);
ids = new int[itemCount]; ids = Arrays.copyOf(itemIds, itemCount);
durationsUs = new long[itemCount]; durationsUs = new long[itemCount];
defaultPositionsUs = new long[itemCount]; defaultPositionsUs = new long[itemCount];
for (MediaQueueItem item : items) { isLive = new boolean[itemCount];
int itemId = item.getItemId(); for (int i = 0; i < ids.length; i++) {
ids[index] = itemId; int id = ids[i];
idsToIndex.put(itemId, index); idsToIndex.put(id, i);
MediaInfo mediaInfo = item.getMedia(); ItemData data = itemIdToData.get(id, ItemData.EMPTY);
String contentId = mediaInfo.getContentId(); durationsUs[i] = data.durationUs;
durationsUs[index] = defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs;
contentIdToDurationUsMap.containsKey(contentId) isLive[i] = data.isLive;
? contentIdToDurationUsMap.get(contentId)
: CastUtils.getStreamDurationUs(mediaInfo);
defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
index++;
} }
} }
@ -73,17 +121,19 @@ import java.util.Map;
} }
@Override @Override
public Window getWindow( public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex]; long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET; boolean isDynamic = durationUs == C.TIME_UNSET;
Object tag = setTag ? ids[windowIndex] : null;
return window.set( return window.set(
tag, /* uid= */ ids[windowIndex],
/* tag= */ ids[windowIndex],
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic, /* isSeekable= */ !isDynamic,
isDynamic, isDynamic,
isLive[windowIndex],
defaultPositionsUs[windowIndex], defaultPositionsUs[windowIndex],
durationUs, durationUs,
/* firstPeriodIndex= */ windowIndex, /* firstPeriodIndex= */ windowIndex,
@ -124,7 +174,8 @@ import java.util.Map;
CastTimeline that = (CastTimeline) other; CastTimeline that = (CastTimeline) other;
return Arrays.equals(ids, that.ids) return Arrays.equals(ids, that.ids)
&& Arrays.equals(durationsUs, that.durationsUs) && Arrays.equals(durationsUs, that.durationsUs)
&& Arrays.equals(defaultPositionsUs, that.defaultPositionsUs); && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs)
&& Arrays.equals(isLive, that.isLive);
} }
@Override @Override
@ -132,6 +183,7 @@ import java.util.Map;
int result = Arrays.hashCode(ids); int result = Arrays.hashCode(ids);
result = 31 * result + Arrays.hashCode(durationsUs); result = 31 * result + Arrays.hashCode(durationsUs);
result = 31 * result + Arrays.hashCode(defaultPositionsUs); result = 31 * result + Arrays.hashCode(defaultPositionsUs);
result = 31 * result + Arrays.hashCode(isLive);
return result; return result;
} }

View File

@ -15,53 +15,94 @@
*/ */
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaStatus;
import java.util.HashMap; import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
/** /**
* Creates {@link CastTimeline}s from cast receiver app media status. * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates.
* *
* <p>This class keeps track of the duration reported by the current item to fill any missing * <p>This class keeps track of the duration reported by the current item to fill any missing
* durations in the media queue items [See internal: b/65152553]. * durations in the media queue items [See internal: b/65152553].
*/ */
/* package */ final class CastTimelineTracker { /* package */ final class CastTimelineTracker {
private final HashMap<String, Long> contentIdToDurationUsMap; private final SparseArray<CastTimeline.ItemData> itemIdToData;
private final HashSet<String> scratchContentIdSet;
public CastTimelineTracker() { public CastTimelineTracker() {
contentIdToDurationUsMap = new HashMap<>(); itemIdToData = new SparseArray<>();
scratchContentIdSet = new HashSet<>();
} }
/** /**
* Returns a {@link CastTimeline} that represent the given {@code status}. * Returns a {@link CastTimeline} that represents the state of the given {@code
* remoteMediaClient}.
* *
* @param status The Cast media status. * <p>Returned timelines may contain values obtained from {@code remoteMediaClient} in previous
* @return A {@link CastTimeline} that represent the given {@code status}. * invocations of this method.
*
* @param remoteMediaClient The Cast media client.
* @return A {@link CastTimeline} that represents the given {@code remoteMediaClient} status.
*/ */
public CastTimeline getCastTimeline(MediaStatus status) { public CastTimeline getCastTimeline(RemoteMediaClient remoteMediaClient) {
MediaInfo mediaInfo = status.getMediaInfo(); int[] itemIds = remoteMediaClient.getMediaQueue().getItemIds();
List<MediaQueueItem> items = status.getQueueItems(); if (itemIds.length > 0) {
removeUnusedDurationEntries(items); // Only remove unused items when there is something in the queue to avoid removing all entries
// if the remote media client clears the queue temporarily. See [Internal ref: b/128825216].
if (mediaInfo != null) { removeUnusedItemDataEntries(itemIds);
String contentId = mediaInfo.getContentId();
long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
contentIdToDurationUsMap.put(contentId, durationUs);
} }
return new CastTimeline(items, contentIdToDurationUsMap);
// TODO: Reset state when the app instance changes [Internal ref: b/129672468].
MediaStatus mediaStatus = remoteMediaClient.getMediaStatus();
if (mediaStatus == null) {
return CastTimeline.EMPTY_CAST_TIMELINE;
}
int currentItemId = mediaStatus.getCurrentItemId();
updateItemData(
currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET);
for (MediaQueueItem item : mediaStatus.getQueueItems()) {
long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs);
}
return new CastTimeline(itemIds, itemIdToData);
} }
private void removeUnusedDurationEntries(List<MediaQueueItem> items) { private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) {
scratchContentIdSet.clear(); CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY);
for (MediaQueueItem item : items) { long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
scratchContentIdSet.add(item.getMedia().getContentId()); if (durationUs == C.TIME_UNSET) {
durationUs = previousData.durationUs;
}
boolean isLive =
mediaInfo == null
? previousData.isLive
: mediaInfo.getStreamType() == MediaInfo.STREAM_TYPE_LIVE;
if (defaultPositionUs == C.TIME_UNSET) {
defaultPositionUs = previousData.defaultPositionUs;
}
itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive));
}
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);
} }
} }

View File

@ -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());
} }

View File

@ -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();
} }
} }

View File

@ -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;
}
}

View File

@ -16,43 +16,32 @@
package com.google.android.exoplayer2.ext.cast; package com.google.android.exoplayer2.ext.cast;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
/** Representation of an item that can be played by a media player. */ /** Representation of a media item. */
public final class MediaItem { public final class MediaItem {
/** A builder for {@link MediaItem} instances. */ /** A builder for {@link MediaItem} instances. */
public static final class Builder { public static final class Builder {
@Nullable private UUID uuid; @Nullable private Uri uri;
private String title; @Nullable private String title;
private String description; @Nullable private String mimeType;
private MediaItem.UriBundle media; @Nullable private DrmConfiguration drmConfiguration;
@Nullable private Object attachment;
private List<MediaItem.DrmScheme> drmSchemes;
private long startPositionUs;
private long endPositionUs;
private String mimeType;
/** Creates an builder with default field values. */ /** See {@link MediaItem#uri}. */
public Builder() { public Builder setUri(String uri) {
clearInternal(); return setUri(Uri.parse(uri));
} }
/** See {@link MediaItem#uuid}. */ /** See {@link MediaItem#uri}. */
public Builder setUuid(UUID uuid) { public Builder setUri(Uri uri) {
this.uuid = uuid; this.uri = uri;
return this; return this;
} }
@ -62,307 +51,125 @@ public final class MediaItem {
return this; return this;
} }
/** See {@link MediaItem#description}. */
public Builder setDescription(String description) {
this.description = description;
return this;
}
/** Equivalent to {@link #setMedia(UriBundle) setMedia(new UriBundle(Uri.parse(uri)))}. */
public Builder setMedia(String uri) {
return setMedia(new UriBundle(Uri.parse(uri)));
}
/** See {@link MediaItem#media}. */
public Builder setMedia(UriBundle media) {
this.media = media;
return this;
}
/** See {@link MediaItem#attachment}. */
public Builder setAttachment(Object attachment) {
this.attachment = attachment;
return this;
}
/** See {@link MediaItem#drmSchemes}. */
public Builder setDrmSchemes(List<MediaItem.DrmScheme> drmSchemes) {
this.drmSchemes = Collections.unmodifiableList(new ArrayList<>(drmSchemes));
return this;
}
/** See {@link MediaItem#startPositionUs}. */
public Builder setStartPositionUs(long startPositionUs) {
this.startPositionUs = startPositionUs;
return this;
}
/** See {@link MediaItem#endPositionUs}. */
public Builder setEndPositionUs(long endPositionUs) {
Assertions.checkArgument(endPositionUs != C.TIME_END_OF_SOURCE);
this.endPositionUs = endPositionUs;
return this;
}
/** See {@link MediaItem#mimeType}. */ /** See {@link MediaItem#mimeType}. */
public Builder setMimeType(String mimeType) { public Builder setMimeType(String mimeType) {
this.mimeType = mimeType; this.mimeType = mimeType;
return this; return this;
} }
/** /** See {@link MediaItem#drmConfiguration}. */
* Equivalent to {@link #build()}, except it also calls {@link #clear()} after creating the public Builder setDrmConfiguration(DrmConfiguration drmConfiguration) {
* {@link MediaItem}. this.drmConfiguration = drmConfiguration;
*/
public MediaItem buildAndClear() {
MediaItem item = build();
clearInternal();
return item;
}
/** Returns the builder to default values. */
public Builder clear() {
clearInternal();
return this; return this;
} }
/** /** Returns a new {@link MediaItem} instance with the current builder values. */
* Returns a new {@link MediaItem} instance with the current builder values. This method also
* clears any values passed to {@link #setUuid(UUID)}.
*/
public MediaItem build() { public MediaItem build() {
UUID uuid = this.uuid; Assertions.checkNotNull(uri);
this.uuid = null; return new MediaItem(uri, title, mimeType, drmConfiguration);
return new MediaItem(
uuid != null ? uuid : UUID.randomUUID(),
title,
description,
media,
attachment,
drmSchemes,
startPositionUs,
endPositionUs,
mimeType);
}
@EnsuresNonNull({"title", "description", "media", "drmSchemes", "mimeType"})
private void clearInternal(@UnknownInitialization Builder this) {
uuid = null;
title = "";
description = "";
media = UriBundle.EMPTY;
attachment = null;
drmSchemes = Collections.emptyList();
startPositionUs = C.TIME_UNSET;
endPositionUs = C.TIME_UNSET;
mimeType = "";
} }
} }
/** Bundles a resource's URI with headers to attach to any request to that URI. */ /** DRM configuration for a media item. */
public static final class UriBundle { public static final class DrmConfiguration {
/** An empty {@link UriBundle}. */
public static final UriBundle EMPTY = new UriBundle(Uri.EMPTY);
/** A URI. */
public final Uri uri;
/** The headers to attach to any request for the given URI. */
public final Map<String, String> requestHeaders;
/**
* Creates an instance with no request headers.
*
* @param uri See {@link #uri}.
*/
public UriBundle(Uri uri) {
this(uri, Collections.emptyMap());
}
/**
* Creates an instance with the given URI and request headers.
*
* @param uri See {@link #uri}.
* @param requestHeaders See {@link #requestHeaders}.
*/
public UriBundle(Uri uri, Map<String, String> requestHeaders) {
this.uri = uri;
this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders));
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
UriBundle uriBundle = (UriBundle) other;
return uri.equals(uriBundle.uri) && requestHeaders.equals(uriBundle.requestHeaders);
}
@Override
public int hashCode() {
int result = uri.hashCode();
result = 31 * result + requestHeaders.hashCode();
return result;
}
}
/**
* Represents a DRM protection scheme, and optionally provides information about how to acquire
* the license for the media.
*/
public static final class DrmScheme {
/** The UUID of the protection scheme. */ /** The UUID of the protection scheme. */
public final UUID uuid; public final UUID uuid;
/** /**
* Optional {@link UriBundle} for the license server. If no license server is provided, the * Optional license server {@link Uri}. If {@code null} then the license server must be
* server must be provided by the media. * specified by the media.
*/ */
@Nullable public final UriBundle licenseServer; @Nullable public final Uri licenseUri;
/** Headers that should be attached to any license requests. */
public final Map<String, String> requestHeaders;
/** /**
* Creates an instance. * Creates an instance.
* *
* @param uuid See {@link #uuid}. * @param uuid See {@link #uuid}.
* @param licenseServer See {@link #licenseServer}. * @param licenseUri See {@link #licenseUri}.
* @param requestHeaders See {@link #requestHeaders}.
*/ */
public DrmScheme(UUID uuid, @Nullable UriBundle licenseServer) { public DrmConfiguration(
UUID uuid, @Nullable Uri licenseUri, @Nullable Map<String, String> requestHeaders) {
this.uuid = uuid; this.uuid = uuid;
this.licenseServer = licenseServer; this.licenseUri = licenseUri;
this.requestHeaders =
requestHeaders == null
? Collections.emptyMap()
: Collections.unmodifiableMap(requestHeaders);
} }
@Override @Override
public boolean equals(@Nullable Object other) { public boolean equals(@Nullable Object obj) {
if (this == other) { if (this == obj) {
return true; return true;
} }
if (other == null || getClass() != other.getClass()) { if (obj == null || getClass() != obj.getClass()) {
return false; return false;
} }
DrmScheme drmScheme = (DrmScheme) other; DrmConfiguration other = (DrmConfiguration) obj;
return uuid.equals(drmScheme.uuid) && Util.areEqual(licenseServer, drmScheme.licenseServer); return uuid.equals(other.uuid)
&& Util.areEqual(licenseUri, other.licenseUri)
&& requestHeaders.equals(other.requestHeaders);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = uuid.hashCode(); int result = uuid.hashCode();
result = 31 * result + (licenseServer != null ? licenseServer.hashCode() : 0); result = 31 * result + (licenseUri != null ? licenseUri.hashCode() : 0);
result = 31 * result + requestHeaders.hashCode();
return result; return result;
} }
} }
/** /** The media {@link Uri}. */
* A UUID that identifies this item, potentially across different devices. The default value is public final Uri uri;
* obtained by calling {@link UUID#randomUUID()}.
*/
public final UUID uuid;
/** The title of the item. The default value is an empty string. */ /** The title of the item, or {@code null} if unspecified. */
public final String title; @Nullable public final String title;
/** A description for the item. The default value is an empty string. */ /** The mime type for the media, or {@code null} if unspecified. */
public final String description; @Nullable public final String mimeType;
/** /** Optional {@link DrmConfiguration} for the media. */
* A {@link UriBundle} to fetch the media content. The default value is {@link UriBundle#EMPTY}. @Nullable public final DrmConfiguration drmConfiguration;
*/
public final UriBundle media;
/** private MediaItem(
* An optional opaque object to attach to the media item. Handling of this attachment is Uri uri,
* implementation specific. The default value is null. @Nullable String title,
*/ @Nullable String mimeType,
@Nullable public final Object attachment; @Nullable DrmConfiguration drmConfiguration) {
this.uri = uri;
/** this.title = title;
* Immutable list of {@link DrmScheme} instances sorted in decreasing order of preference. The this.mimeType = mimeType;
* default value is an empty list. this.drmConfiguration = drmConfiguration;
*/ }
public final List<DrmScheme> drmSchemes;
/**
* The position in microseconds at which playback of this media item should start. {@link
* C#TIME_UNSET} if playback should start at the default position. The default value is {@link
* C#TIME_UNSET}.
*/
public final long startPositionUs;
/**
* The position in microseconds at which playback of this media item should end. {@link
* C#TIME_UNSET} if playback should end at the end of the media. The default value is {@link
* C#TIME_UNSET}.
*/
public final long endPositionUs;
/**
* The mime type of this media item. The default value is an empty string.
*
* <p>The usage of this mime type is optional and player implementation specific.
*/
public final String mimeType;
// TODO: Add support for sideloaded tracks, artwork, icon, and subtitle.
@Override @Override
public boolean equals(@Nullable Object other) { public boolean equals(@Nullable Object obj) {
if (this == other) { if (this == obj) {
return true; return true;
} }
if (other == null || getClass() != other.getClass()) { if (obj == null || getClass() != obj.getClass()) {
return false; return false;
} }
MediaItem mediaItem = (MediaItem) other; MediaItem other = (MediaItem) obj;
return startPositionUs == mediaItem.startPositionUs return uri.equals(other.uri)
&& endPositionUs == mediaItem.endPositionUs && Util.areEqual(title, other.title)
&& uuid.equals(mediaItem.uuid) && Util.areEqual(mimeType, other.mimeType)
&& title.equals(mediaItem.title) && Util.areEqual(drmConfiguration, other.drmConfiguration);
&& description.equals(mediaItem.description)
&& media.equals(mediaItem.media)
&& Util.areEqual(attachment, mediaItem.attachment)
&& drmSchemes.equals(mediaItem.drmSchemes)
&& mimeType.equals(mediaItem.mimeType);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = uuid.hashCode(); int result = uri.hashCode();
result = 31 * result + title.hashCode(); result = 31 * result + (title == null ? 0 : title.hashCode());
result = 31 * result + description.hashCode(); result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
result = 31 * result + media.hashCode(); result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
result = 31 * result + (attachment != null ? attachment.hashCode() : 0);
result = 31 * result + drmSchemes.hashCode();
result = 31 * result + (int) (startPositionUs ^ (startPositionUs >>> 32));
result = 31 * result + (int) (endPositionUs ^ (endPositionUs >>> 32));
result = 31 * result + mimeType.hashCode();
return result; return result;
} }
private MediaItem(
UUID uuid,
String title,
String description,
UriBundle media,
@Nullable Object attachment,
List<DrmScheme> drmSchemes,
long startPositionUs,
long endPositionUs,
String mimeType) {
this.uuid = uuid;
this.title = title;
this.description = description;
this.media = media;
this.attachment = attachment;
this.drmSchemes = drmSchemes;
this.startPositionUs = startPositionUs;
this.endPositionUs = endPositionUs;
this.mimeType = mimeType;
}
} }

View File

@ -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);
}

View File

@ -1,85 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
/** Represents a sequence of {@link MediaItem MediaItems}. */
public interface MediaItemQueue {
/**
* Returns the item at the given index.
*
* @param index The index of the item to retrieve.
* @return The item at the given index.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
MediaItem get(int index);
/** Returns the number of items in this queue. */
int getSize();
/**
* Appends the given sequence of items to the queue.
*
* @param items The sequence of items to append.
*/
void add(MediaItem... items);
/**
* Adds the given sequence of items to the queue at the given position, so that the first of
* {@code items} is placed at the given index.
*
* @param index The index at which {@code items} will be inserted.
* @param items The sequence of items to append.
* @throws IndexOutOfBoundsException If {@code index < 0 || index > getSize()}.
*/
void add(int index, MediaItem... items);
/**
* Moves an existing item within the playlist.
*
* <p>Calling this method is equivalent to removing the item at position {@code indexFrom} and
* immediately inserting it at position {@code indexTo}. If the moved item is being played at the
* moment of the invocation, playback will stick with the moved item.
*
* @param indexFrom The index of the item to move.
* @param indexTo The index at which the item will be placed after this operation.
* @throws IndexOutOfBoundsException If for either index, {@code index < 0 || index >= getSize()}.
*/
void move(int indexFrom, int indexTo);
/**
* Removes an item from the queue.
*
* @param index The index of the item to remove from the queue.
* @throws IndexOutOfBoundsException If {@code index < 0 || index >= getSize()}.
*/
void remove(int index);
/**
* Removes a range of items from the queue.
*
* <p>Does nothing if an empty range ({@code from == exclusiveTo}) is passed.
*
* @param from The inclusive index at which the range to remove starts.
* @param exclusiveTo The exclusive index at which the range to remove ends.
* @throws IndexOutOfBoundsException If {@code from < 0 || exclusiveTo > getSize() || from >
* exclusiveTo}.
*/
void removeRange(int from, int exclusiveTo);
/** Removes all items in the queue. */
void clear();
}

View File

@ -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;

View File

@ -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>

View File

@ -0,0 +1,185 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Player;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManager;
import com.google.android.gms.cast.framework.media.MediaQueue;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
/** Tests for {@link CastPlayer}. */
@RunWith(AndroidJUnit4.class)
public class CastPlayerTest {
private CastPlayer castPlayer;
private RemoteMediaClient.Listener remoteMediaClientListener;
@Mock private RemoteMediaClient mockRemoteMediaClient;
@Mock private MediaStatus mockMediaStatus;
@Mock private MediaQueue mockMediaQueue;
@Mock private CastContext mockCastContext;
@Mock private SessionManager mockSessionManager;
@Mock private CastSession mockCastSession;
@Mock private Player.EventListener mockListener;
@Mock private PendingResult<RemoteMediaClient.MediaChannelResult> mockPendingResult;
@Captor
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
setResultCallbackArgumentCaptor;
@Captor private ArgumentCaptor<RemoteMediaClient.Listener> listenerArgumentCaptor;
@Before
public void setUp() {
initMocks(this);
when(mockCastContext.getSessionManager()).thenReturn(mockSessionManager);
when(mockSessionManager.getCurrentCastSession()).thenReturn(mockCastSession);
when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient);
when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus);
when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue);
when(mockMediaQueue.getItemIds()).thenReturn(new int[0]);
// Make the remote media client present the same default values as ExoPlayer:
when(mockRemoteMediaClient.isPaused()).thenReturn(true);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
castPlayer = new CastPlayer(mockCastContext);
castPlayer.addListener(mockListener);
verify(mockRemoteMediaClient).addListener(listenerArgumentCaptor.capture());
remoteMediaClientListener = listenerArgumentCaptor.getValue();
}
@Test
public void testSetPlayWhenReady_masksRemoteState() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.play();
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
// There is a status update in the middle, which should be hidden by masking.
remoteMediaClientListener.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the remoteMediaClient has updated its state according to the play() call.
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@Test
public void testSetPlayWhenReadyMasking_updatesUponResultChange() {
when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
castPlayer.play();
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getPlayWhenReady()).isTrue();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
// Upon result, the remote media client is still paused. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE);
assertThat(castPlayer.getPlayWhenReady()).isFalse();
}
@Test
public void testPlayWhenReady_changesOnStatusUpdates() {
assertThat(castPlayer.getPlayWhenReady()).isFalse();
when(mockRemoteMediaClient.isPaused()).thenReturn(false);
remoteMediaClientListener.onStatusUpdated();
verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE);
assertThat(castPlayer.getPlayWhenReady()).isTrue();
}
@Test
public void testSetRepeatMode_masksRemoteState() {
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
// There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the mediaStatus now exposes the new repeat mode.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verifyNoMoreInteractions(mockListener);
}
@Test
public void testSetRepeatMode_updatesUponResultChange() {
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
castPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture());
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
// There is a status update in the middle, which should be hidden by masking.
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_ALL);
remoteMediaClientListener.onStatusUpdated();
verifyNoMoreInteractions(mockListener);
// Upon result, the repeat mode is ALL. The state should reflect that.
setResultCallbackArgumentCaptor
.getValue()
.onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class));
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ALL);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL);
}
@Test
public void testRepeatMode_changesOnStatusUpdates() {
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_SINGLE);
remoteMediaClientListener.onStatusUpdated();
verify(mockListener).onRepeatModeChanged(Player.REPEAT_MODE_ONE);
assertThat(castPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE);
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -18,127 +18,69 @@ package com.google.android.exoplayer2.ext.cast;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Test for {@link MediaItem}. */ /** Test for {@link MediaItem}. */
@RunWith(RobolectricTestRunner.class) @RunWith(AndroidJUnit4.class)
public class MediaItemTest { public class MediaItemTest {
@Test
public void buildMediaItem_resetsUuid() {
MediaItem.Builder builder = new MediaItem.Builder();
UUID uuid = new UUID(1, 1);
MediaItem item1 = builder.setUuid(uuid).build();
MediaItem item2 = builder.build();
MediaItem item3 = builder.build();
assertThat(item1.uuid).isEqualTo(uuid);
assertThat(item2.uuid).isNotEqualTo(uuid);
assertThat(item3.uuid).isNotEqualTo(item2.uuid);
assertThat(item3.uuid).isNotEqualTo(uuid);
}
@Test @Test
public void buildMediaItem_doesNotChangeState() { public void buildMediaItem_doesNotChangeState() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder = new MediaItem.Builder();
MediaItem item1 = MediaItem item1 =
builder builder
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("http://example.com"))
.setMedia("http://example.com")
.setTitle("title") .setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4) .setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.build(); .build();
MediaItem item2 = builder.setUuid(new UUID(0, 1)).build(); MediaItem item2 = builder.build();
assertThat(item1).isEqualTo(item2); assertThat(item1).isEqualTo(item2);
} }
@Test
public void buildMediaItem_assertDefaultValues() {
assertDefaultValues(new MediaItem.Builder().build());
}
@Test
public void buildAndClear_assertDefaultValues() {
MediaItem.Builder builder = new MediaItem.Builder();
builder
.setMedia("http://example.com")
.setTitle("title")
.setMimeType(MimeTypes.AUDIO_MP4)
.setStartPositionUs(3)
.setEndPositionUs(4)
.buildAndClear();
assertDefaultValues(builder.build());
}
@Test @Test
public void equals_withEqualDrmSchemes_returnsTrue() { public void equals_withEqualDrmSchemes_returnsTrue() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 = MediaItem mediaItem1 =
builder builder1
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear(); MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 = MediaItem mediaItem2 =
builder builder2
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear();
assertThat(mediaItem1).isEqualTo(mediaItem2); assertThat(mediaItem1).isEqualTo(mediaItem2);
} }
@Test @Test
public void equals_withDifferentDrmRequestHeaders_returnsFalse() { public void equals_withDifferentDrmRequestHeaders_returnsFalse() {
MediaItem.Builder builder = new MediaItem.Builder(); MediaItem.Builder builder1 = new MediaItem.Builder();
MediaItem mediaItem1 = MediaItem mediaItem1 =
builder builder1
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(1))
.setDrmSchemes(createDummyDrmSchemes(1)) .build();
.buildAndClear(); MediaItem.Builder builder2 = new MediaItem.Builder();
MediaItem mediaItem2 = MediaItem mediaItem2 =
builder builder2
.setUuid(new UUID(0, 1)) .setUri(Uri.parse("www.google.com"))
.setMedia("www.google.com") .setDrmConfiguration(buildDrmConfiguration(2))
.setDrmSchemes(createDummyDrmSchemes(2)) .build();
.buildAndClear();
assertThat(mediaItem1).isNotEqualTo(mediaItem2); assertThat(mediaItem1).isNotEqualTo(mediaItem2);
} }
private static void assertDefaultValues(MediaItem item) { private static MediaItem.DrmConfiguration buildDrmConfiguration(int seed) {
assertThat(item.title).isEmpty(); HashMap<String, String> requestHeaders = new HashMap<>();
assertThat(item.description).isEmpty(); requestHeaders.put("key1", "value1");
assertThat(item.media.uri).isEqualTo(Uri.EMPTY); requestHeaders.put("key2", "value2" + seed);
assertThat(item.attachment).isNull(); return new MediaItem.DrmConfiguration(
assertThat(item.drmSchemes).isEmpty(); C.WIDEVINE_UUID, Uri.parse("www.uri1.com"), requestHeaders);
assertThat(item.startPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.endPositionUs).isEqualTo(C.TIME_UNSET);
assertThat(item.mimeType).isEmpty();
}
private static List<MediaItem.DrmScheme> createDummyDrmSchemes(int seed) {
HashMap<String, String> requestHeaders1 = new HashMap<>();
requestHeaders1.put("key1", "value1");
requestHeaders1.put("key2", "value1");
MediaItem.UriBundle uriBundle1 =
new MediaItem.UriBundle(Uri.parse("www.uri1.com"), requestHeaders1);
MediaItem.DrmScheme drmScheme1 = new MediaItem.DrmScheme(C.WIDEVINE_UUID, uriBundle1);
HashMap<String, String> requestHeaders2 = new HashMap<>();
requestHeaders2.put("key3", "value3");
requestHeaders2.put("key4", "valueWithSeed" + seed);
MediaItem.UriBundle uriBundle2 =
new MediaItem.UriBundle(Uri.parse("www.uri2.com"), requestHeaders2);
MediaItem.DrmScheme drmScheme2 = new MediaItem.DrmScheme(C.PLAYREADY_UUID, uriBundle2);
return Arrays.asList(drmScheme1, drmScheme2);
} }
} }

View File

@ -1 +0,0 @@
manifest=src/test/AndroidManifest.xml

View File

@ -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

View File

@ -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:76.3809.111'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils-robolectric') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
ext { ext {

View File

@ -15,12 +15,13 @@
*/ */
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 android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
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.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;
@ -35,12 +36,14 @@ import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; 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;
@ -51,7 +54,9 @@ import org.chromium.net.UrlResponseInfo;
/** /**
* DataSource without intermediate buffer based on Cronet API set using UrlRequest. * DataSource without intermediate buffer based on Cronet API set using UrlRequest.
* *
* <p>This class's methods are organized in the sequence of expected calls. * <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
* priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
* construct the instance.
*/ */
public class CronetDataSource extends BaseDataSource implements HttpDataSource { public class CronetDataSource extends BaseDataSource implements HttpDataSource {
@ -113,16 +118,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;
@ -130,18 +136,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;
@ -153,21 +159,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);
} }
/** /**
@ -177,32 +177,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);
} }
/** /**
@ -212,29 +208,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,
@ -243,21 +235,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;
@ -268,6 +366,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
@ -285,12 +394,20 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
requestProperties.clear(); requestProperties.clear();
} }
@Override
public int getResponseCode() {
return responseInfo == null || responseInfo.getHttpStatusCode() <= 0
? -1
: responseInfo.getHttpStatusCode();
}
@Override @Override
public Map<String, List<String>> getResponseHeaders() { public Map<String, List<String>> getResponseHeaders() {
return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders(); return responseInfo == null ? Collections.emptyMap() : responseInfo.getAllHeaders();
} }
@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());
} }
@ -303,22 +420,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();
@ -326,6 +444,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
} }
// Check for a valid response code. // Check for a valid response code.
UrlResponseInfo responseInfo = Assertions.checkNotNull(this.responseInfo);
int responseCode = responseInfo.getHttpStatusCode(); int responseCode = responseInfo.getHttpStatusCode();
if (responseCode < 200 || responseCode > 299) { if (responseCode < 200 || responseCode > 299) {
InvalidResponseCodeException exception = InvalidResponseCodeException exception =
@ -333,7 +452,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
responseCode, responseCode,
responseInfo.getHttpStatusText(), responseInfo.getHttpStatusText(),
responseInfo.getAllHeaders(), responseInfo.getAllHeaders(),
currentDataSpec); dataSpec);
if (responseCode == 416) { if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE)); exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
} }
@ -341,11 +460,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);
} }
} }
@ -355,7 +475,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 {
@ -364,7 +484,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;
@ -383,37 +503,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 {
@ -438,6 +540,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) {
@ -476,29 +687,25 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
cronetEngine cronetEngine
.newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor) .newUrlRequestBuilder(dataSpec.uri.toString(), urlRequestCallback, executor)
.allowDirectExecutor(); .allowDirectExecutor();
// Set the headers. // Set the headers.
boolean isContentTypeHeaderSet = false; Map<String, String> requestHeaders = new HashMap<>();
if (defaultRequestProperties != null) { if (defaultRequestProperties != null) {
for (Entry<String, String> headerEntry : defaultRequestProperties.getSnapshot().entrySet()) { requestHeaders.putAll(defaultRequestProperties.getSnapshot());
String key = headerEntry.getKey();
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
requestBuilder.addHeader(key, headerEntry.getValue());
}
} }
Map<String, String> requestPropertiesSnapshot = requestProperties.getSnapshot(); requestHeaders.putAll(requestProperties.getSnapshot());
for (Entry<String, String> headerEntry : requestPropertiesSnapshot.entrySet()) { requestHeaders.putAll(dataSpec.httpRequestHeaders);
for (Entry<String, String> headerEntry : requestHeaders.entrySet()) {
String key = headerEntry.getKey(); String key = headerEntry.getKey();
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key); String value = headerEntry.getValue();
requestBuilder.addHeader(key, headerEntry.getValue()); requestBuilder.addHeader(key, value);
} }
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
if (dataSpec.httpBody != null && !requestHeaders.containsKey(CONTENT_TYPE)) {
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();
@ -510,7 +717,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)) {
@ -539,7 +746,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");
@ -617,10 +866,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
@ -629,13 +890,15 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (request != currentUrlRequest) { if (request != currentUrlRequest) {
return; return;
} }
if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) { UrlRequest urlRequest = Assertions.checkNotNull(currentUrlRequest);
DataSpec dataSpec = Assertions.checkNotNull(currentDataSpec);
if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
int responseCode = info.getHttpStatusCode(); int responseCode = info.getHttpStatusCode();
// The industry standard is to disregard POST redirects when the status code is 307 or 308. // The industry standard is to disregard POST redirects when the status code is 307 or 308.
if (responseCode == 307 || responseCode == 308) { if (responseCode == 307 || responseCode == 308) {
exception = exception =
new InvalidResponseCodeException( new InvalidResponseCodeException(
responseCode, info.getHttpStatusText(), info.getAllHeaders(), currentDataSpec); responseCode, info.getHttpStatusText(), info.getAllHeaders(), dataSpec);
operation.open(); operation.open();
return; return;
} }
@ -644,40 +907,47 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
resetConnectTimeout(); resetConnectTimeout();
} }
Map<String, List<String>> headers = info.getAllHeaders(); if (!handleSetCookieRequests) {
if (!handleSetCookieRequests || isEmpty(headers.get(SET_COOKIE))) {
request.followRedirect(); request.followRedirect();
} else { return;
currentUrlRequest.cancel();
DataSpec redirectUrlDataSpec;
if (currentDataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
// transformed into a GET.
redirectUrlDataSpec =
new DataSpec(
Uri.parse(newLocationUrl),
DataSpec.HTTP_METHOD_GET,
/* httpBody= */ null,
currentDataSpec.absoluteStreamPosition,
currentDataSpec.position,
currentDataSpec.length,
currentDataSpec.key,
currentDataSpec.flags);
} else {
redirectUrlDataSpec = currentDataSpec.withUri(Uri.parse(newLocationUrl));
}
UrlRequest.Builder requestBuilder;
try {
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
} catch (IOException e) {
exception = e;
return;
}
String cookieHeadersValue = parseCookies(headers.get(SET_COOKIE));
attachCookies(requestBuilder, cookieHeadersValue);
currentUrlRequest = requestBuilder.build();
currentUrlRequest.start();
} }
List<String> setCookieHeaders = info.getAllHeaders().get(SET_COOKIE);
if (isEmpty(setCookieHeaders)) {
request.followRedirect();
return;
}
urlRequest.cancel();
DataSpec redirectUrlDataSpec;
if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
// For POST redirects that aren't 307 or 308, the redirect is followed but request is
// transformed into a GET.
redirectUrlDataSpec =
new DataSpec(
Uri.parse(newLocationUrl),
DataSpec.HTTP_METHOD_GET,
/* httpBody= */ null,
dataSpec.absoluteStreamPosition,
dataSpec.position,
dataSpec.length,
dataSpec.key,
dataSpec.flags,
dataSpec.httpRequestHeaders);
} else {
redirectUrlDataSpec = dataSpec.withUri(Uri.parse(newLocationUrl));
}
UrlRequest.Builder requestBuilder;
try {
requestBuilder = buildRequestBuilder(redirectUrlDataSpec);
} catch (IOException e) {
exception = e;
return;
}
String cookieHeadersValue = parseCookies(setCookieHeaders);
attachCookies(requestBuilder, cookieHeadersValue);
currentUrlRequest = requestBuilder.build();
currentUrlRequest.start();
} }
@Override @Override

View File

@ -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,

View File

@ -16,7 +16,8 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import android.content.Context; import android.content.Context;
import android.support.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
@ -37,8 +38,8 @@ public final class CronetEngineWrapper {
private static final String TAG = "CronetEngineWrapper"; private static final String TAG = "CronetEngineWrapper";
private final CronetEngine cronetEngine; @Nullable private final CronetEngine cronetEngine;
private final @CronetEngineSource int cronetEngineSource; @CronetEngineSource private final int cronetEngineSource;
/** /**
* Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link * Source of {@link CronetEngine}. One of {@link #SOURCE_NATIVE}, {@link #SOURCE_GMS}, {@link
@ -144,7 +145,8 @@ public final class CronetEngineWrapper {
* *
* @return A {@link CronetEngineSource} value. * @return A {@link CronetEngineSource} value.
*/ */
public @CronetEngineSource int getCronetEngineSource() { @CronetEngineSource
public int getCronetEngineSource() {
return cronetEngineSource; return cronetEngineSource;
} }
@ -153,13 +155,14 @@ public final class CronetEngineWrapper {
* *
* @return The CronetEngine, or null if no CronetEngine is available. * @return The CronetEngine, or null if no CronetEngine is available.
*/ */
@Nullable
/* package */ CronetEngine getCronetEngine() { /* package */ CronetEngine getCronetEngine() {
return cronetEngine; return cronetEngine;
} }
private static class CronetProviderComparator implements Comparator<CronetProvider> { private static class CronetProviderComparator implements Comparator<CronetProvider> {
private final String gmsCoreCronetName; @Nullable private final String gmsCoreCronetName;
private final boolean preferGMSCoreCronet; private final boolean preferGMSCoreCronet;
// Multi-catch can only be used for API 19+ in this case. // Multi-catch can only be used for API 19+ in this case.

View File

@ -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;

View File

@ -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>

View File

@ -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};

View File

@ -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;
@ -60,12 +60,12 @@ import org.chromium.net.impl.UrlResponseInfoImpl;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
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 +85,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 +94,25 @@ 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);
HttpDataSource.RequestProperties defaultRequestProperties =
new HttpDataSource.RequestProperties();
defaultRequestProperties.set("defaultHeader1", "defaultValue1");
defaultRequestProperties.set("defaultHeader2", "defaultValue2");
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,
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);
@ -193,18 +196,59 @@ public final class CronetDataSourceTest {
} }
@Test @Test
public void testRequestHeadersSet() throws HttpDataSourceException { public void testRequestSetsRangeHeader() throws HttpDataSourceException {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null);
mockResponseStartSuccess(); mockResponseStartSuccess();
dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue");
dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue");
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1. // The header value to add is current position to current position + length - 1.
verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999"); verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999");
verify(mockUrlRequestBuilder).addHeader("firstHeader", "firstValue"); }
verify(mockUrlRequestBuilder).addHeader("secondHeader", "secondValue");
@Test
public void testRequestHeadersSet() throws HttpDataSourceException {
Map<String, String> headersSet = new HashMap<>();
doAnswer(
(invocation) -> {
String key = invocation.getArgument(0);
String value = invocation.getArgument(1);
headersSet.put(key, value);
return null;
})
.when(mockUrlRequestBuilder)
.addHeader(ArgumentMatchers.anyString(), ArgumentMatchers.anyString());
dataSourceUnderTest.setRequestProperty("defaultHeader2", "dataSourceOverridesDefault");
dataSourceUnderTest.setRequestProperty("dataSourceHeader1", "dataSourceValue1");
dataSourceUnderTest.setRequestProperty("dataSourceHeader2", "dataSourceValue2");
Map<String, String> dataSpecRequestProperties = new HashMap<>();
dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");
testDataSpec =
new DataSpec(
/* uri= */ Uri.parse(TEST_URL),
/* httpMethod= */ DataSpec.HTTP_METHOD_GET,
/* httpBody= */ null,
/* absoluteStreamPosition= */ 1000,
/* position= */ 1000,
/* length= */ 5000,
/* key= */ null,
/* flags= */ 0,
dataSpecRequestProperties);
mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec);
assertThat(headersSet.get("defaultHeader1")).isEqualTo("defaultValue1");
assertThat(headersSet.get("defaultHeader2")).isEqualTo("dataSourceOverridesDefault");
assertThat(headersSet.get("defaultHeader3")).isEqualTo("dataSpecOverridesAll");
assertThat(headersSet.get("dataSourceHeader1")).isEqualTo("dataSourceValue1");
assertThat(headersSet.get("dataSourceHeader2")).isEqualTo("dataSpecOverridesDataSource");
assertThat(headersSet.get("dataSpecHeader1")).isEqualTo("dataSpecValue1");
verify(mockUrlRequest).start(); verify(mockUrlRequest).start();
} }
@ -245,6 +289,26 @@ public final class CronetDataSourceTest {
} }
} }
@Test
public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
testDataSpec =
new DataSpec(
/* uri= */ Uri.parse(TEST_URL),
/* postBody= */ new byte[1024],
/* absoluteStreamPosition= */ 200,
/* position= */ 200,
/* length= */ 1024,
/* key= */ "key",
/* flags= */ 0);
try {
dataSourceUnderTest.open(testDataSpec);
fail();
} catch (IOException expected) {
// Expected
}
}
@Test @Test
public void testRequestOpenFailDueToDnsFailure() { public void testRequestOpenFailDueToDnsFailure() {
mockResponseStartFailure(); mockResponseStartFailure();
@ -283,7 +347,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 +362,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 +622,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 +1059,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 +1089,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 +1127,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 +1177,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 +1238,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 +1447,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;
}
} }

View File

@ -1 +0,0 @@
manifest=src/test/AndroidManifest.xml

Some files were not shown because too many files have changed in this diff Show More