mirror of
https://github.com/androidx/media.git
synced 2025-05-09 00:20:45 +08:00
commit
ffea2a64b3
@ -1,5 +1,66 @@
|
|||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
### 2.12.2 (2020-12-01) ###
|
||||||
|
|
||||||
|
* Core library:
|
||||||
|
* Suppress exceptions from registering/unregistering the stream volume
|
||||||
|
receiver ([#8087](https://github.com/google/ExoPlayer/issues/8087)),
|
||||||
|
([#8106](https://github.com/google/ExoPlayer/issues/8106)).
|
||||||
|
* Suppress ProGuard warnings caused by Guava's compile-only dependencies
|
||||||
|
([#8103](https://github.com/google/ExoPlayer/issues/8103)).
|
||||||
|
* Fix issue that could cause playback to freeze when selecting tracks, if
|
||||||
|
extension audio renderers are being used
|
||||||
|
([#8203](https://github.com/google/ExoPlayer/issues/8203)).
|
||||||
|
* UI:
|
||||||
|
* Fix incorrect color and text alignment of the `StyledPlayerControlView`
|
||||||
|
fast forward and rewind buttons, when used together with the
|
||||||
|
`com.google.android.material` library
|
||||||
|
([#7898](https://github.com/google/ExoPlayer/issues/7898)).
|
||||||
|
* Add `dispatchPrepare(Player)` to `ControlDispatcher` and implement it in
|
||||||
|
`DefaultControlDispatcher`. Deprecate `PlaybackPreparer` and
|
||||||
|
`setPlaybackPreparer` in `StyledPlayerView`, `StyledPlayerControlView`,
|
||||||
|
`PlayerView`, `PlayerControlView`, `PlayerNotificationManager` and
|
||||||
|
`LeanbackPlayerAdapter` and use `ControlDispatcher` for dispatching
|
||||||
|
prepare instead
|
||||||
|
([#7882](https://github.com/google/ExoPlayer/issues/7882)).
|
||||||
|
* Increase seekbar's touch target height in `StyledPlayerControlView`.
|
||||||
|
* Update `StyledPlayerControlView` menu items to behave correctly for
|
||||||
|
right-to-left languages.
|
||||||
|
* Support enabling the previous and next actions individually in
|
||||||
|
`PlayerNotificationManager`.
|
||||||
|
* Audio:
|
||||||
|
* Retry playback after some types of `AudioTrack` error.
|
||||||
|
* Work around `AudioManager` crashes when calling `getStreamVolume`
|
||||||
|
([#8191](https://github.com/google/ExoPlayer/issues/8191)).
|
||||||
|
* Extractors:
|
||||||
|
* Matroska: Add support for 32-bit floating point PCM, and 8-bit and
|
||||||
|
16-bit big endian integer PCM
|
||||||
|
([#8142](https://github.com/google/ExoPlayer/issues/8142)).
|
||||||
|
* MP4: Add support for mpeg1 video box
|
||||||
|
([#8257](https://github.com/google/ExoPlayer/issues/8257)).
|
||||||
|
* IMA extension:
|
||||||
|
* Upgrade IMA SDK dependency to 3.21.0, and release the `AdsLoader`
|
||||||
|
([#7344](https://github.com/google/ExoPlayer/issues/7344)).
|
||||||
|
* Improve handling of ad tags with unsupported VPAID ads
|
||||||
|
([#7832](https://github.com/google/ExoPlayer/issues/7832)).
|
||||||
|
* Fix a bug that caused multiple ads in an ad pod to be skipped when one
|
||||||
|
ad in the ad pod was skipped.
|
||||||
|
* Fix a bug that caused ad progress not to be updated if the player
|
||||||
|
resumed after buffering during an ad
|
||||||
|
([#8239](https://github.com/google/ExoPlayer/issues/8239)).
|
||||||
|
* Fix passing an ads response to the `ImaAdsLoader` builder.
|
||||||
|
* Set the overlay language based on the device locale by default.
|
||||||
|
* Cronet extension:
|
||||||
|
* Fix handling of HTTP status code 200 when making unbounded length range
|
||||||
|
requests ([#8090](https://github.com/google/ExoPlayer/issues/8090)).
|
||||||
|
* Text
|
||||||
|
* Allow tx3g subtitles with `styl` boxes with start and/or end offsets
|
||||||
|
that lie outside the length of the cue text.
|
||||||
|
* Media2 extension:
|
||||||
|
* Notify onBufferingEnded when the state of origin player becomes
|
||||||
|
STATE_IDLE or STATE_ENDED.
|
||||||
|
* Allow to remove all playlist items that makes the player reset.
|
||||||
|
|
||||||
### 2.12.1 (2020-10-23) ###
|
### 2.12.1 (2020-10-23) ###
|
||||||
|
|
||||||
* Core library:
|
* Core library:
|
||||||
@ -7,6 +68,7 @@
|
|||||||
argument ([#8024](https://github.com/google/ExoPlayer/issues/8024)).
|
argument ([#8024](https://github.com/google/ExoPlayer/issues/8024)).
|
||||||
* Fix bug where streams with highly uneven track durations may get stuck
|
* Fix bug where streams with highly uneven track durations may get stuck
|
||||||
in a buffering state
|
in a buffering state
|
||||||
|
([#7943](https://github.com/google/ExoPlayer/issues/7943)).
|
||||||
* Switch Guava dependency from `implementation` to `api`
|
* Switch Guava dependency from `implementation` to `api`
|
||||||
([#7905](https://github.com/google/ExoPlayer/issues/7905),
|
([#7905](https://github.com/google/ExoPlayer/issues/7905),
|
||||||
[#7993](https://github.com/google/ExoPlayer/issues/7993)).
|
[#7993](https://github.com/google/ExoPlayer/issues/7993)).
|
||||||
@ -54,6 +116,9 @@
|
|||||||
([#7992](https://github.com/google/ExoPlayer/issues/7992)).
|
([#7992](https://github.com/google/ExoPlayer/issues/7992)).
|
||||||
* FLV: Make files seekable by using the key frame index
|
* FLV: Make files seekable by using the key frame index
|
||||||
([#7378](https://github.com/google/ExoPlayer/issues/7378)).
|
([#7378](https://github.com/google/ExoPlayer/issues/7378)).
|
||||||
|
* Downloads: Fix issue retrying progressive downloads, which could also result
|
||||||
|
in a crash in `DownloadManager.InternalHandler.onContentLengthChanged`
|
||||||
|
([#8078](https://github.com/google/ExoPlayer/issues/8078).
|
||||||
* HLS: Fix crash affecting chunkful preparation of master playlists that start
|
* HLS: Fix crash affecting chunkful preparation of master playlists that start
|
||||||
with an I-FRAME only variant
|
with an I-FRAME only variant
|
||||||
([#8025](https://github.com/google/ExoPlayer/issues/8025)).
|
([#8025](https://github.com/google/ExoPlayer/issues/8025)).
|
||||||
@ -63,12 +128,12 @@
|
|||||||
* Allow apps to specify a `VideoAdPlayerCallback`
|
* Allow apps to specify a `VideoAdPlayerCallback`
|
||||||
([#7944](https://github.com/google/ExoPlayer/issues/7944)).
|
([#7944](https://github.com/google/ExoPlayer/issues/7944)).
|
||||||
* Accept ad tags via the `AdsMediaSource` constructor and deprecate
|
* Accept ad tags via the `AdsMediaSource` constructor and deprecate
|
||||||
passing them via the `ImaAdsLoader` constructor/builders. Passing the
|
passing them via the `ImaAdsLoader` constructor/builders. Passing the ad
|
||||||
ad tag via media item playback properties continues to be supported.
|
tag via media item playback properties continues to be supported. This
|
||||||
This is in preparation for supporting ads in playlists
|
is in preparation for supporting ads in playlists
|
||||||
([#3750](https://github.com/google/ExoPlayer/issues/3750)).
|
([#3750](https://github.com/google/ExoPlayer/issues/3750)).
|
||||||
* Add a way to override ad media MIME types
|
* Add a way to override ad media MIME types
|
||||||
([#7961)(https://github.com/google/ExoPlayer/issues/7961)).
|
([#7961](https://github.com/google/ExoPlayer/issues/7961)).
|
||||||
* Fix incorrect truncation of large cue point positions
|
* Fix incorrect truncation of large cue point positions
|
||||||
([#8067](https://github.com/google/ExoPlayer/issues/8067)).
|
([#8067](https://github.com/google/ExoPlayer/issues/8067)).
|
||||||
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for
|
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.12.1'
|
releaseVersion = '2.12.2'
|
||||||
releaseVersionCode = 2012001
|
releaseVersionCode = 2012002
|
||||||
minSdkVersion = 16
|
minSdkVersion = 16
|
||||||
appTargetSdkVersion = 29
|
appTargetSdkVersion = 29
|
||||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
||||||
|
@ -1,31 +1,117 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "YouTube DASH",
|
"name": "Clear DASH",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Google Glass H264 (MP4)",
|
"name": "HD (MP4, H264)",
|
||||||
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0",
|
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
|
||||||
"extension": "mpd"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Play H264 (MP4)",
|
"name": "UHD (MP4, H264)",
|
||||||
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0",
|
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
|
||||||
"extension": "mpd"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Glass VP9 (WebM)",
|
"name": "HD (MP4, H265)",
|
||||||
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3.7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0",
|
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
|
||||||
"extension": "mpd"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Google Play VP9 (WebM)",
|
"name": "UHD (MP4, H265)",
|
||||||
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD.BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0",
|
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
|
||||||
"extension": "mpd"
|
},
|
||||||
|
{
|
||||||
|
"name": "HD (WebM, VP9)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UHD (WebM, VP9)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Widevine GTS policy tests",
|
"name": "Widevine DASH (MP4, H264)",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "HD (cenc)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UHD (cenc)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HD (cbcs)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UHD (cbcs)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Secure -> Clear -> Secure (cenc)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
||||||
|
"drm_session_for_clear_content": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Widevine DASH (WebM, VP9)",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "HD (cenc, full-sample)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UHD (cenc, full-sample)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HD (cenc, sub-sample)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UHD (cenc, sub-sample)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Widevine DASH (MP4, H265)",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "HD (cenc)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UHD (cenc)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Widevine DASH (policy tests)",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "SW secure crypto (L3)",
|
"name": "SW secure crypto (L3)",
|
||||||
@ -102,143 +188,27 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Widevine DASH H264 (MP4)",
|
"name": "60fps DASH",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Clear",
|
"name": "HD (MP4, H264, Clear)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Clear UHD",
|
"name": "4K (MP4, H264, Clear)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Secure (cenc)",
|
"name": "HD (MP4, H264, Widevine cenc)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd",
|
||||||
"drm_scheme": "widevine",
|
"drm_scheme": "widevine",
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Secure UHD (cenc)",
|
"name": "4K (MP4, H264, Widevine cenc)",
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd",
|
||||||
"drm_scheme": "widevine",
|
"drm_scheme": "widevine",
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure (cbcs)",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure UHD (cbcs)",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs_uhd.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure -> Clear -> Secure (cenc)",
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/widevine/tears_enc_clear_enc.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test",
|
|
||||||
"drm_session_for_clear_content": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Widevine DASH VP9 (WebM)",
|
|
||||||
"samples": [
|
|
||||||
{
|
|
||||||
"name": "Clear",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears.mpd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Clear UHD",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure (full-sample)",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure UHD (full-sample)",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure (sub-sample)",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure UHD (sub-sample)",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Widevine DASH H265 (MP4)",
|
|
||||||
"samples": [
|
|
||||||
{
|
|
||||||
"name": "Clear",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Clear UHD",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.mpd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure UHD",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears_uhd.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Widevine AV1 (WebM)",
|
|
||||||
"samples": [
|
|
||||||
{
|
|
||||||
"name": "Clear",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/2019/clear/av1/24/webm/llama_av1_480p_400.webm"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure L3",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Secure L1",
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "SmoothStreaming",
|
|
||||||
"samples": [
|
|
||||||
{
|
|
||||||
"name": "Super speed",
|
|
||||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Super speed (PlayReady)",
|
|
||||||
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
|
|
||||||
"drm_scheme": "playready"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -246,11 +216,11 @@
|
|||||||
"name": "HLS",
|
"name": "HLS",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Apple 4x3 basic stream",
|
"name": "Apple 4x3 basic stream (TS)",
|
||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple 16x9 basic stream",
|
"name": "Apple 16x9 basic stream (TS)",
|
||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -262,146 +232,26 @@
|
|||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple TS media playlist",
|
"name": "Apple media playlist (TS)",
|
||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple AAC media playlist",
|
"name": "Apple media playlist (AAC)",
|
||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Misc",
|
"name": "SmoothStreaming",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Dizzy (MP4)",
|
"name": "Super speed (MP4, H264, Clear)",
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple 10s (AAC)",
|
"name": "Super speed (MP4, H264, PlayReady)",
|
||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
|
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
|
||||||
},
|
"drm_scheme": "playready"
|
||||||
{
|
|
||||||
"name": "Apple 10s (TS)",
|
|
||||||
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Screens audio (FMP4)",
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Google Play (MP3)",
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Google Play (Ogg/Vorbis)",
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Big Buck Bunny 480p video (MP4,AV1)",
|
|
||||||
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "One hour frame counter (MP4)",
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Playlists",
|
|
||||||
"samples": [
|
|
||||||
{
|
|
||||||
"name": "Cats -> Dogs",
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Audio -> Video -> Audio",
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"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/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Manual ad insertion",
|
|
||||||
"playlist": [
|
|
||||||
{
|
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
|
||||||
"clip_end_position_ms": 10000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
|
||||||
"clip_end_position_ms": 5000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
|
||||||
"clip_start_position_ms": 10000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -497,6 +347,105 @@
|
|||||||
"name": "VMAP midroll at 1765 s",
|
"name": "VMAP midroll at 1765 s",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||||
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-large"
|
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VMAP midroll ad pod at 5 s with 10 skippable ads",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||||
|
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-10-skippable-ads"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Playlists",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "Cats -> Dogs",
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Audio -> Video -> Audio",
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"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/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Clear -> Enc -> Clear -> Enc -> Enc",
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_sd.mpd",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Manual ad insertion",
|
||||||
|
"playlist": [
|
||||||
|
{
|
||||||
|
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||||
|
"clip_end_position_ms": 10000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
|
||||||
|
"clip_end_position_ms": 5000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||||
|
"clip_start_position_ms": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AV1",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"name": "SD (WebM, Clear)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/2019/clear/av1/24/webm/llama_av1_480p_400.webm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SD (WebM, Widevine cenc, L3)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_SW_SECURE_CRYPTO&provider=widevine_test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SD (WebM, Widevine cenc, L1)",
|
||||||
|
"uri": "https://storage.googleapis.com/wvmedia/2019/cenc/av1/24/webm/llama_av1_480p_400.webm",
|
||||||
|
"drm_scheme": "widevine",
|
||||||
|
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?video_id=GTS_HW_SECURE_ALL&provider=widevine_test"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -504,68 +453,104 @@
|
|||||||
"name": "Subtitles",
|
"name": "Subtitles",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "TTML",
|
"name": "TTML positioning",
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
|
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
|
||||||
"subtitle_mime_type": "application/ttml+xml",
|
"subtitle_mime_type": "application/ttml+xml",
|
||||||
"subtitle_language": "en"
|
"subtitle_language": "en"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "WebVTT line positioning",
|
"name": "TTML Japanese features",
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
|
||||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt",
|
|
||||||
"subtitle_mime_type": "text/vtt",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MPEG-4 Timed Text (tx3g, mov_text)",
|
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Japanese features (vertical + rubies) [TTML]",
|
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/japanese-ttml.xml",
|
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/japanese-ttml.xml",
|
||||||
"subtitle_mime_type": "application/ttml+xml",
|
"subtitle_mime_type": "application/ttml+xml",
|
||||||
"subtitle_language": "ja"
|
"subtitle_language": "ja"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Japanese features (vertical + rubies) [WebVTT]",
|
"name": "WebVTT positioning",
|
||||||
|
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||||
|
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt",
|
||||||
|
"subtitle_mime_type": "text/vtt",
|
||||||
|
"subtitle_language": "en"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WebVTT Japanese features",
|
||||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/japanese.vtt",
|
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/japanese.vtt",
|
||||||
"subtitle_mime_type": "text/vtt",
|
"subtitle_mime_type": "text/vtt",
|
||||||
"subtitle_language": "ja"
|
"subtitle_language": "ja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SubStation Alpha positioning",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MPEG-4 Timed Text",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "60fps",
|
"name": "Misc",
|
||||||
"samples": [
|
"samples": [
|
||||||
{
|
{
|
||||||
"name": "Big Buck Bunny (DASH,H264,1080p,Clear)",
|
"name": "Dizzy (MP4)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd"
|
"uri": "https://html5demos.com/assets/dizzy.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Big Buck Bunny (DASH,H264,4K,Clear)",
|
"name": "Apple 10s (AAC)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd"
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Big Buck Bunny (DASH,H264,1080p,Widevine)",
|
"name": "Apple 10s (TS)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd",
|
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
|
||||||
"drm_scheme": "widevine",
|
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Big Buck Bunny (DASH,H264,4K,Widevine)",
|
"name": "Android screens (MKV)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd",
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"
|
||||||
"drm_scheme": "widevine",
|
},
|
||||||
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
{
|
||||||
|
"name": "Screens 360p (WebM, VP9)",
|
||||||
|
"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)",
|
||||||
|
"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)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-137.mp4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Screens audio (FMP4)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/audio-141.mp4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Google Play (MP3)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Google Play (Ogg, Vorbis)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/ogg/play.ogg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Big Buck Bunny 480p (MP4, AV1)",
|
||||||
|
"uri": "https://storage.googleapis.com/downloads.webmproject.org/av1/exoplayer/bbb-av1-480p.mp4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "One hour frame counter (MP4)",
|
||||||
|
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -98,20 +98,20 @@ public class DownloadTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDownloaded(MediaItem mediaItem) {
|
public boolean isDownloaded(MediaItem mediaItem) {
|
||||||
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
@Nullable Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||||
return download != null && download.state != Download.STATE_FAILED;
|
return download != null && download.state != Download.STATE_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public DownloadRequest getDownloadRequest(Uri uri) {
|
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||||
Download download = downloads.get(uri);
|
@Nullable Download download = downloads.get(uri);
|
||||||
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleDownload(
|
public void toggleDownload(
|
||||||
FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
|
FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
|
||||||
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
@Nullable Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
|
||||||
if (download != null) {
|
if (download != null && download.state != Download.STATE_FAILED) {
|
||||||
DownloadService.sendRemoveDownload(
|
DownloadService.sendRemoveDownload(
|
||||||
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
|
||||||
} else {
|
} else {
|
||||||
@ -223,7 +223,7 @@ public class DownloadTracker {
|
|||||||
widevineOfflineLicenseFetchTask =
|
widevineOfflineLicenseFetchTask =
|
||||||
new WidevineOfflineLicenseFetchTask(
|
new WidevineOfflineLicenseFetchTask(
|
||||||
format,
|
format,
|
||||||
mediaItem.playbackProperties.drmConfiguration.licenseUri,
|
mediaItem.playbackProperties.drmConfiguration,
|
||||||
httpDataSourceFactory,
|
httpDataSourceFactory,
|
||||||
/* dialogHelper= */ this,
|
/* dialogHelper= */ this,
|
||||||
helper);
|
helper);
|
||||||
@ -373,7 +373,7 @@ public class DownloadTracker {
|
|||||||
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
|
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
|
||||||
|
|
||||||
private final Format format;
|
private final Format format;
|
||||||
private final Uri licenseUri;
|
private final MediaItem.DrmConfiguration drmConfiguration;
|
||||||
private final HttpDataSource.Factory httpDataSourceFactory;
|
private final HttpDataSource.Factory httpDataSourceFactory;
|
||||||
private final StartDownloadDialogHelper dialogHelper;
|
private final StartDownloadDialogHelper dialogHelper;
|
||||||
private final DownloadHelper downloadHelper;
|
private final DownloadHelper downloadHelper;
|
||||||
@ -383,12 +383,12 @@ public class DownloadTracker {
|
|||||||
|
|
||||||
public WidevineOfflineLicenseFetchTask(
|
public WidevineOfflineLicenseFetchTask(
|
||||||
Format format,
|
Format format,
|
||||||
Uri licenseUri,
|
MediaItem.DrmConfiguration drmConfiguration,
|
||||||
HttpDataSource.Factory httpDataSourceFactory,
|
HttpDataSource.Factory httpDataSourceFactory,
|
||||||
StartDownloadDialogHelper dialogHelper,
|
StartDownloadDialogHelper dialogHelper,
|
||||||
DownloadHelper downloadHelper) {
|
DownloadHelper downloadHelper) {
|
||||||
this.format = format;
|
this.format = format;
|
||||||
this.licenseUri = licenseUri;
|
this.drmConfiguration = drmConfiguration;
|
||||||
this.httpDataSourceFactory = httpDataSourceFactory;
|
this.httpDataSourceFactory = httpDataSourceFactory;
|
||||||
this.dialogHelper = dialogHelper;
|
this.dialogHelper = dialogHelper;
|
||||||
this.downloadHelper = downloadHelper;
|
this.downloadHelper = downloadHelper;
|
||||||
@ -398,8 +398,10 @@ public class DownloadTracker {
|
|||||||
protected Void doInBackground(Void... voids) {
|
protected Void doInBackground(Void... voids) {
|
||||||
OfflineLicenseHelper offlineLicenseHelper =
|
OfflineLicenseHelper offlineLicenseHelper =
|
||||||
OfflineLicenseHelper.newWidevineInstance(
|
OfflineLicenseHelper.newWidevineInstance(
|
||||||
licenseUri.toString(),
|
drmConfiguration.licenseUri.toString(),
|
||||||
|
drmConfiguration.forceDefaultLicenseUri,
|
||||||
httpDataSourceFactory,
|
httpDataSourceFactory,
|
||||||
|
drmConfiguration.requestHeaders,
|
||||||
new DrmSessionEventListener.EventDispatcher());
|
new DrmSessionEventListener.EventDispatcher());
|
||||||
try {
|
try {
|
||||||
keySetId = offlineLicenseHelper.downloadLicense(format);
|
keySetId = offlineLicenseHelper.downloadLicense(format);
|
||||||
|
@ -35,7 +35,6 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||||||
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.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
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;
|
||||||
@ -66,10 +65,11 @@ import java.net.CookiePolicy;
|
|||||||
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.Map;
|
||||||
|
|
||||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||||
public class PlayerActivity extends AppCompatActivity
|
public class PlayerActivity extends AppCompatActivity
|
||||||
implements OnClickListener, PlaybackPreparer, StyledPlayerControlView.VisibilityListener {
|
implements OnClickListener, StyledPlayerControlView.VisibilityListener {
|
||||||
|
|
||||||
// Saved instance state keys.
|
// Saved instance state keys.
|
||||||
|
|
||||||
@ -252,13 +252,6 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaybackPreparer implementation
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void preparePlayback() {
|
|
||||||
player.prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayerControlView.VisibilityListener implementation
|
// PlayerControlView.VisibilityListener implementation
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -304,7 +297,6 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
|
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
|
||||||
player.setPlayWhenReady(startAutoPlay);
|
player.setPlayWhenReady(startAutoPlay);
|
||||||
playerView.setPlayer(player);
|
playerView.setPlayer(player);
|
||||||
playerView.setPlaybackPreparer(this);
|
|
||||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||||
debugViewHelper.start();
|
debugViewHelper.start();
|
||||||
}
|
}
|
||||||
@ -335,6 +327,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
|
|
||||||
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
|
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
|
||||||
showToast(R.string.error_cleartext_not_permitted);
|
showToast(R.string.error_cleartext_not_permitted);
|
||||||
|
finish();
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) {
|
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) {
|
||||||
@ -551,7 +544,9 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
.setCustomCacheKey(downloadRequest.customCacheKey)
|
.setCustomCacheKey(downloadRequest.customCacheKey)
|
||||||
.setMimeType(downloadRequest.mimeType)
|
.setMimeType(downloadRequest.mimeType)
|
||||||
.setStreamKeys(downloadRequest.streamKeys)
|
.setStreamKeys(downloadRequest.streamKeys)
|
||||||
.setDrmKeySetId(downloadRequest.keySetId);
|
.setDrmKeySetId(downloadRequest.keySetId)
|
||||||
|
.setDrmLicenseRequestHeaders(getDrmRequestHeaders(item));
|
||||||
|
|
||||||
mediaItems.add(builder.build());
|
mediaItems.add(builder.build());
|
||||||
} else {
|
} else {
|
||||||
mediaItems.add(item);
|
mediaItems.add(item);
|
||||||
@ -559,4 +554,10 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
return mediaItems;
|
return mediaItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Map<String, String> getDrmRequestHeaders(MediaItem item) {
|
||||||
|
MediaItem.DrmConfiguration drmConfiguration = item.playbackProperties.drmConfiguration;
|
||||||
|
return drmConfiguration != null ? drmConfiguration.requestHeaders : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<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 HTTP traffic not permitted. See https://exoplayer.dev/issues/cleartext-not-permitted</string>
|
||||||
|
|
||||||
<string name="error_generic">Playback failed</string>
|
<string name="error_generic">Playback failed</string>
|
||||||
|
|
||||||
|
@ -27,6 +27,10 @@ import com.google.android.gms.cast.MediaTrack;
|
|||||||
*/
|
*/
|
||||||
/* package */ final class CastUtils {
|
/* package */ final class CastUtils {
|
||||||
|
|
||||||
|
/** The duration returned by {@link MediaInfo#getStreamDuration()} for live streams. */
|
||||||
|
// TODO: Remove once [Internal ref: b/171657375] is fixed.
|
||||||
|
private static final long LIVE_STREAM_DURATION = -1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
|
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
|
||||||
* unknown or not applicable.
|
* unknown or not applicable.
|
||||||
@ -39,7 +43,9 @@ import com.google.android.gms.cast.MediaTrack;
|
|||||||
return C.TIME_UNSET;
|
return C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
long durationMs = mediaInfo.getStreamDuration();
|
long durationMs = mediaInfo.getStreamDuration();
|
||||||
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
|
return durationMs != MediaInfo.UNKNOWN_DURATION && durationMs != LIVE_STREAM_DURATION
|
||||||
|
? C.msToUs(durationMs)
|
||||||
|
: C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -443,8 +443,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
transferInitializing(dataSpec);
|
transferInitializing(dataSpec);
|
||||||
try {
|
try {
|
||||||
boolean connectionOpened = blockUntilConnectTimeout();
|
boolean connectionOpened = blockUntilConnectTimeout();
|
||||||
if (exception != null) {
|
@Nullable IOException connectionOpenException = exception;
|
||||||
throw new OpenException(exception, dataSpec, getStatus(urlRequest));
|
if (connectionOpenException != null) {
|
||||||
|
@Nullable String message = connectionOpenException.getMessage();
|
||||||
|
if (message != null
|
||||||
|
&& Util.toLowerInvariant(message).contains("err_cleartext_not_permitted")) {
|
||||||
|
throw new CleartextNotPermittedException(connectionOpenException, dataSpec);
|
||||||
|
}
|
||||||
|
throw new OpenException(connectionOpenException, 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(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
|
throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
|
||||||
@ -506,7 +512,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||||
bytesRemaining = dataSpec.length;
|
bytesRemaining = dataSpec.length;
|
||||||
} else {
|
} else {
|
||||||
bytesRemaining = getContentLength(responseInfo);
|
long contentLength = getContentLength(responseInfo);
|
||||||
|
bytesRemaining =
|
||||||
|
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
|
||||||
}
|
}
|
||||||
} 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
|
||||||
|
@ -534,7 +534,8 @@ public final class CronetDataSourceTest {
|
|||||||
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
long length = dataSourceUnderTest.open(testDataSpec);
|
||||||
|
assertThat(length).isEqualTo(5000);
|
||||||
|
|
||||||
byte[] returnedBuffer = new byte[16];
|
byte[] returnedBuffer = new byte[16];
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||||
@ -551,7 +552,26 @@ public final class CronetDataSourceTest {
|
|||||||
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
long length = dataSourceUnderTest.open(testDataSpec);
|
||||||
|
assertThat(length).isEqualTo(5000);
|
||||||
|
|
||||||
|
byte[] returnedBuffer = new byte[16];
|
||||||
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||||
|
assertThat(bytesRead).isEqualTo(16);
|
||||||
|
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
|
||||||
|
verify(mockTransferListener)
|
||||||
|
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unboundedRangeRequestWith200Response() throws HttpDataSourceException {
|
||||||
|
mockResponseStartSuccess();
|
||||||
|
mockReadSuccess(0, (int) TEST_CONTENT_LENGTH);
|
||||||
|
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||||
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, C.LENGTH_UNSET);
|
||||||
|
|
||||||
|
long length = dataSourceUnderTest.open(testDataSpec);
|
||||||
|
assertThat(length).isEqualTo(TEST_CONTENT_LENGTH - 1000);
|
||||||
|
|
||||||
byte[] returnedBuffer = new byte[16];
|
byte[] returnedBuffer = new byte[16];
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
|
||||||
@ -777,7 +797,8 @@ public final class CronetDataSourceTest {
|
|||||||
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
long length = dataSourceUnderTest.open(testDataSpec);
|
||||||
|
assertThat(length).isEqualTo(5000);
|
||||||
|
|
||||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
@ -796,7 +817,8 @@ public final class CronetDataSourceTest {
|
|||||||
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
|
||||||
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
|
||||||
|
|
||||||
dataSourceUnderTest.open(testDataSpec);
|
long length = dataSourceUnderTest.open(testDataSpec);
|
||||||
|
assertThat(length).isEqualTo(5000);
|
||||||
|
|
||||||
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
|
||||||
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
|
||||||
|
@ -25,7 +25,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.20.1'
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.21.0'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
||||||
|
@ -47,8 +47,10 @@ import com.google.android.exoplayer2.testutil.HostActivity;
|
|||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -78,7 +80,7 @@ public final class ImaPlaybackTest {
|
|||||||
@Test
|
@Test
|
||||||
public void playbackWithPrerollAdTag_playsAdAndContent() throws Exception {
|
public void playbackWithPrerollAdTag_playsAdAndContent() throws Exception {
|
||||||
String adsResponse =
|
String adsResponse =
|
||||||
TestUtil.getString(/* context= */ testRule.getActivity(), "ad-responses/preroll.xml");
|
TestUtil.getString(/* context= */ testRule.getActivity(), "media/ad-responses/preroll.xml");
|
||||||
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT};
|
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT};
|
||||||
ImaHostedTest hostedTest =
|
ImaHostedTest hostedTest =
|
||||||
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
||||||
@ -90,7 +92,8 @@ public final class ImaPlaybackTest {
|
|||||||
public void playbackWithMidrolls_playsAdAndContent() throws Exception {
|
public void playbackWithMidrolls_playsAdAndContent() throws Exception {
|
||||||
String adsResponse =
|
String adsResponse =
|
||||||
TestUtil.getString(
|
TestUtil.getString(
|
||||||
/* context= */ testRule.getActivity(), "ad-responses/preroll_midroll6s_postroll.xml");
|
/* context= */ testRule.getActivity(),
|
||||||
|
"media/ad-responses/preroll_midroll6s_postroll.xml");
|
||||||
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT, ad(1), CONTENT, ad(2), CONTENT};
|
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT, ad(1), CONTENT, ad(2), CONTENT};
|
||||||
ImaHostedTest hostedTest =
|
ImaHostedTest hostedTest =
|
||||||
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
||||||
@ -102,7 +105,7 @@ public final class ImaPlaybackTest {
|
|||||||
public void playbackWithMidrolls1And7_playsAdsAndContent() throws Exception {
|
public void playbackWithMidrolls1And7_playsAdsAndContent() throws Exception {
|
||||||
String adsResponse =
|
String adsResponse =
|
||||||
TestUtil.getString(
|
TestUtil.getString(
|
||||||
/* context= */ testRule.getActivity(), "ad-responses/midroll1s_midroll7s.xml");
|
/* context= */ testRule.getActivity(), "media/ad-responses/midroll1s_midroll7s.xml");
|
||||||
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
||||||
ImaHostedTest hostedTest =
|
ImaHostedTest hostedTest =
|
||||||
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
|
||||||
@ -114,7 +117,7 @@ public final class ImaPlaybackTest {
|
|||||||
public void playbackWithMidrolls10And20WithSeekTo12_playsAdsAndContent() throws Exception {
|
public void playbackWithMidrolls10And20WithSeekTo12_playsAdsAndContent() throws Exception {
|
||||||
String adsResponse =
|
String adsResponse =
|
||||||
TestUtil.getString(
|
TestUtil.getString(
|
||||||
/* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml");
|
/* context= */ testRule.getActivity(), "media/ad-responses/midroll10s_midroll20s.xml");
|
||||||
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
||||||
ImaHostedTest hostedTest =
|
ImaHostedTest hostedTest =
|
||||||
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
|
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
|
||||||
@ -131,7 +134,7 @@ public final class ImaPlaybackTest {
|
|||||||
public void playbackWithMidrolls10And20WithSeekTo18_playsAdsAndContent() throws Exception {
|
public void playbackWithMidrolls10And20WithSeekTo18_playsAdsAndContent() throws Exception {
|
||||||
String adsResponse =
|
String adsResponse =
|
||||||
TestUtil.getString(
|
TestUtil.getString(
|
||||||
/* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml");
|
/* context= */ testRule.getActivity(), "media/ad-responses/midroll10s_midroll20s.xml");
|
||||||
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
|
||||||
ImaHostedTest hostedTest =
|
ImaHostedTest hostedTest =
|
||||||
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
|
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
|
||||||
@ -190,7 +193,7 @@ public final class ImaPlaybackTest {
|
|||||||
private static final class ImaHostedTest extends ExoHostedTest implements EventListener {
|
private static final class ImaHostedTest extends ExoHostedTest implements EventListener {
|
||||||
|
|
||||||
private final Uri contentUri;
|
private final Uri contentUri;
|
||||||
private final String adsResponse;
|
private final DataSpec adTagDataSpec;
|
||||||
private final List<AdId> expectedAdIds;
|
private final List<AdId> expectedAdIds;
|
||||||
private final List<AdId> seenAdIds;
|
private final List<AdId> seenAdIds;
|
||||||
private @MonotonicNonNull ImaAdsLoader imaAdsLoader;
|
private @MonotonicNonNull ImaAdsLoader imaAdsLoader;
|
||||||
@ -201,7 +204,9 @@ public final class ImaPlaybackTest {
|
|||||||
// duration due to ad playback, so the hosted test shouldn't assert the playing duration.
|
// duration due to ad playback, so the hosted test shouldn't assert the playing duration.
|
||||||
super(ImaPlaybackTest.class.getSimpleName(), /* fullPlaybackNoSeeking= */ false);
|
super(ImaPlaybackTest.class.getSimpleName(), /* fullPlaybackNoSeeking= */ false);
|
||||||
this.contentUri = contentUri;
|
this.contentUri = contentUri;
|
||||||
this.adsResponse = adsResponse;
|
this.adTagDataSpec =
|
||||||
|
new DataSpec(
|
||||||
|
Util.getDataUriForString(/* mimeType= */ "text/xml", /* data= */ adsResponse));
|
||||||
this.expectedAdIds = Arrays.asList(expectedAdIds);
|
this.expectedAdIds = Arrays.asList(expectedAdIds);
|
||||||
seenAdIds = new ArrayList<>();
|
seenAdIds = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@ -226,7 +231,7 @@ public final class ImaPlaybackTest {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
Context context = host.getApplicationContext();
|
Context context = host.getApplicationContext();
|
||||||
imaAdsLoader = new ImaAdsLoader.Builder(context).buildForAdsResponse(adsResponse);
|
imaAdsLoader = new ImaAdsLoader.Builder(context).build();
|
||||||
imaAdsLoader.setPlayer(player);
|
imaAdsLoader.setPlayer(player);
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
@ -242,7 +247,8 @@ public final class ImaPlaybackTest {
|
|||||||
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
|
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
|
||||||
return new AdsMediaSource(
|
return new AdsMediaSource(
|
||||||
contentMediaSource,
|
contentMediaSource,
|
||||||
dataSourceFactory,
|
adTagDataSpec,
|
||||||
|
new DefaultMediaSourceFactory(dataSourceFactory),
|
||||||
Assertions.checkNotNull(imaAdsLoader),
|
Assertions.checkNotNull(imaAdsLoader),
|
||||||
new AdViewProvider() {
|
new AdViewProvider() {
|
||||||
|
|
||||||
|
@ -82,6 +82,7 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
@ -705,7 +706,9 @@ public final class ImaAdsLoader
|
|||||||
if (adTagUri != null) {
|
if (adTagUri != null) {
|
||||||
adTagDataSpec = new DataSpec(adTagUri);
|
adTagDataSpec = new DataSpec(adTagUri);
|
||||||
} else if (adsResponse != null) {
|
} else if (adsResponse != null) {
|
||||||
adTagDataSpec = new DataSpec(Util.getDataUriForString(adsResponse, "text/xml"));
|
adTagDataSpec =
|
||||||
|
new DataSpec(
|
||||||
|
Util.getDataUriForString(/* mimeType= */ "text/xml", /* data= */ adsResponse));
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
@ -871,6 +874,7 @@ public final class ImaAdsLoader
|
|||||||
if (configuration.applicationAdErrorListener != null) {
|
if (configuration.applicationAdErrorListener != null) {
|
||||||
adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener);
|
adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener);
|
||||||
}
|
}
|
||||||
|
adsLoader.release();
|
||||||
}
|
}
|
||||||
imaPausedContent = false;
|
imaPausedContent = false;
|
||||||
imaAdState = IMA_AD_STATE_NONE;
|
imaAdState = IMA_AD_STATE_NONE;
|
||||||
@ -1118,6 +1122,10 @@ public final class ImaAdsLoader
|
|||||||
|
|
||||||
private void updateAdProgress() {
|
private void updateAdProgress() {
|
||||||
VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate();
|
VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate();
|
||||||
|
if (configuration.debugModeEnabled) {
|
||||||
|
Log.d(TAG, "Ad progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
|
||||||
|
}
|
||||||
|
|
||||||
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
|
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
|
||||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||||
adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate);
|
adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate);
|
||||||
@ -1211,17 +1219,31 @@ public final class ImaAdsLoader
|
|||||||
if (imaAdInfo != null) {
|
if (imaAdInfo != null) {
|
||||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
|
adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
|
||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
} else if (adPlaybackState.adGroupCount == 1 && adPlaybackState.adGroupTimesUs[0] == 0) {
|
} else {
|
||||||
// For incompatible VPAID ads with one preroll, content is resumed immediately. In this case
|
// Mark any ads for the current/reported player position that haven't loaded as being in the
|
||||||
// we haven't received ad info (the ad never loaded), but there is only one ad group to skip.
|
// error state, to force resuming content. This includes VPAID ads that never load.
|
||||||
adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ 0);
|
long playerPositionUs;
|
||||||
updateAdPlaybackState();
|
if (player != null) {
|
||||||
|
playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period));
|
||||||
|
} else if (!VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(lastContentProgress)) {
|
||||||
|
// Playback is backgrounded so use the last reported content position.
|
||||||
|
playerPositionUs = C.msToUs(lastContentProgress.getCurrentTimeMs());
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int adGroupIndex =
|
||||||
|
adPlaybackState.getAdGroupIndexForPositionUs(
|
||||||
|
playerPositionUs, C.msToUs(contentDurationMs));
|
||||||
|
if (adGroupIndex != C.INDEX_UNSET) {
|
||||||
|
markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||||
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
|
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
|
||||||
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
|
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
|
||||||
|
bufferingAd = true;
|
||||||
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
|
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
|
||||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||||
adCallbacks.get(i).onBuffering(adMediaInfo);
|
adCallbacks.get(i).onBuffering(adMediaInfo);
|
||||||
@ -1282,13 +1304,18 @@ public final class ImaAdsLoader
|
|||||||
if (adMediaInfo == null) {
|
if (adMediaInfo == null) {
|
||||||
Log.w(TAG, "onEnded without ad media info");
|
Log.w(TAG, "onEnded without ad media info");
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
|
||||||
adCallbacks.get(i).onEnded(adMediaInfo);
|
if (playingAdIndexInAdGroup == C.INDEX_UNSET
|
||||||
|
|| (adInfo != null && adInfo.adIndexInAdGroup < playingAdIndexInAdGroup)) {
|
||||||
|
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||||
|
adCallbacks.get(i).onEnded(adMediaInfo);
|
||||||
|
}
|
||||||
|
if (configuration.debugModeEnabled) {
|
||||||
|
Log.d(
|
||||||
|
TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (configuration.debugModeEnabled) {
|
|
||||||
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
|
if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
|
||||||
int adGroupIndex = player.getCurrentAdGroupIndex();
|
int adGroupIndex = player.getCurrentAdGroupIndex();
|
||||||
@ -1716,15 +1743,9 @@ public final class ImaAdsLoader
|
|||||||
public VideoProgressUpdate getContentProgress() {
|
public VideoProgressUpdate getContentProgress() {
|
||||||
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
|
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
|
||||||
if (configuration.debugModeEnabled) {
|
if (configuration.debugModeEnabled) {
|
||||||
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
|
Log.d(
|
||||||
Log.d(TAG, "Content progress: not ready");
|
TAG,
|
||||||
} else {
|
"Content progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
Util.formatInvariant(
|
|
||||||
"Content progress: %.1f of %.1f s",
|
|
||||||
videoProgressUpdate.getCurrentTime(), videoProgressUpdate.getDuration()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) {
|
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) {
|
||||||
@ -1893,7 +1914,9 @@ public final class ImaAdsLoader
|
|||||||
private static final class DefaultImaFactory implements ImaUtil.ImaFactory {
|
private static final class DefaultImaFactory implements ImaUtil.ImaFactory {
|
||||||
@Override
|
@Override
|
||||||
public ImaSdkSettings createImaSdkSettings() {
|
public ImaSdkSettings createImaSdkSettings() {
|
||||||
return ImaSdkFactory.getInstance().createImaSdkSettings();
|
ImaSdkSettings settings = ImaSdkFactory.getInstance().createImaSdkSettings();
|
||||||
|
settings.setLanguage(getImaLanguageCodeForDefaultLocale());
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1934,5 +1957,17 @@ public final class ImaAdsLoader
|
|||||||
return ImaSdkFactory.getInstance()
|
return ImaSdkFactory.getInstance()
|
||||||
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a language code that's suitable for passing to {@link ImaSdkSettings#setLanguage} and
|
||||||
|
* corresponds to the device's {@link Locale#getDefault() default Locale}. IMA will fall back to
|
||||||
|
* its default language code ("en") if the value returned is unsupported.
|
||||||
|
*/
|
||||||
|
// TODO: It may be possible to define a better mapping onto IMA's supported language codes. See:
|
||||||
|
// https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization.
|
||||||
|
// [Internal ref: b/174042000] will help if implemented.
|
||||||
|
private static String getImaLanguageCodeForDefaultLocale() {
|
||||||
|
return Util.splitAtFirst(Util.getSystemLanguageCodes()[0], "-")[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose;
|
|||||||
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
|
||||||
import com.google.ads.interactivemedia.v3.api.UiElement;
|
import com.google.ads.interactivemedia.v3.api.UiElement;
|
||||||
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
|
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
|
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
|
||||||
@ -202,5 +203,16 @@ import java.util.Set;
|
|||||||
|| adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR;
|
|| adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a human-readable representation of a video progress update. */
|
||||||
|
public static String getStringForVideoProgressUpdate(VideoProgressUpdate videoProgressUpdate) {
|
||||||
|
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
|
||||||
|
return "not ready";
|
||||||
|
} else {
|
||||||
|
return Util.formatInvariant(
|
||||||
|
"%d ms of %d ms",
|
||||||
|
videoProgressUpdate.getCurrentTimeMs(), videoProgressUpdate.getDurationMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ImaUtil() {}
|
private ImaUtil() {}
|
||||||
}
|
}
|
||||||
|
@ -78,10 +78,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link PlaybackPreparer}.
|
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The adapter calls
|
||||||
*
|
* {@link ControlDispatcher#dispatchPrepare(Player)} instead of {@link
|
||||||
* @param playbackPreparer The {@link PlaybackPreparer}.
|
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the adapter
|
||||||
|
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
|
||||||
|
* you can provide a custom implementation of {@link
|
||||||
|
* ControlDispatcher#dispatchPrepare(Player)}.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
||||||
this.playbackPreparer = playbackPreparer;
|
this.playbackPreparer = playbackPreparer;
|
||||||
}
|
}
|
||||||
@ -167,11 +172,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
|
|||||||
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
|
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calls deprecated method to provide backwards compatibility.
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public void play() {
|
public void play() {
|
||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
playbackPreparer.preparePlayback();
|
playbackPreparer.preparePlayback();
|
||||||
|
} else {
|
||||||
|
controlDispatcher.dispatchPrepare(player);
|
||||||
}
|
}
|
||||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.ext.media2;
|
package com.google.android.exoplayer2.ext.media2;
|
||||||
|
|
||||||
|
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_IDLE;
|
||||||
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED;
|
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED;
|
||||||
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PLAYING;
|
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PLAYING;
|
||||||
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_INFO_SKIPPED;
|
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_INFO_SKIPPED;
|
||||||
@ -60,6 +61,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@ -762,18 +764,42 @@ public class SessionPlayerConnectorTest {
|
|||||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||||
public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception {
|
public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception {
|
||||||
List<MediaItem> playlist = TestUtils.createPlaylist(10);
|
List<MediaItem> playlist = TestUtils.createPlaylist(10);
|
||||||
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(1);
|
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
|
||||||
sessionPlayerConnector.registerPlayerCallback(
|
sessionPlayerConnector.registerPlayerCallback(executor, callback);
|
||||||
executor, new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch));
|
|
||||||
|
|
||||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
|
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
|
||||||
assertThat(onCurrentMediaItemChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS))
|
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||||
.isTrue();
|
|
||||||
|
|
||||||
assertThat(sessionPlayerConnector.getPlaylist()).isEqualTo(playlist);
|
assertThat(sessionPlayerConnector.getPlaylist()).isEqualTo(playlist);
|
||||||
assertThat(sessionPlayerConnector.getCurrentMediaItem()).isEqualTo(playlist.get(0));
|
assertThat(sessionPlayerConnector.getCurrentMediaItem()).isEqualTo(playlist.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@LargeTest
|
||||||
|
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||||
|
public void setPlaylistAndRemoveAllPlaylistItem_playerStateBecomesIdle() throws Exception {
|
||||||
|
List<MediaItem> playlist = new ArrayList<>();
|
||||||
|
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
|
||||||
|
PlayerCallbackForPlaylist callback =
|
||||||
|
new PlayerCallbackForPlaylist(playlist, 2) {
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(@NonNull SessionPlayer player, int playerState) {
|
||||||
|
countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sessionPlayerConnector.registerPlayerCallback(executor, callback);
|
||||||
|
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
|
||||||
|
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_PAUSED);
|
||||||
|
|
||||||
|
callback.resetLatch(1);
|
||||||
|
assertPlayerResultSuccess(sessionPlayerConnector.removePlaylistItem(0));
|
||||||
|
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||||
|
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_IDLE);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
|
||||||
@ -826,7 +852,6 @@ public class SessionPlayerConnectorTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
|
|
||||||
InstrumentationRegistry.getInstrumentation()
|
InstrumentationRegistry.getInstrumentation()
|
||||||
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
|
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
|
||||||
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||||
@ -959,14 +984,12 @@ public class SessionPlayerConnectorTest {
|
|||||||
int listSize = 2;
|
int listSize = 2;
|
||||||
List<MediaItem> playlist = TestUtils.createPlaylist(listSize);
|
List<MediaItem> playlist = TestUtils.createPlaylist(listSize);
|
||||||
|
|
||||||
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(1);
|
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
|
||||||
sessionPlayerConnector.registerPlayerCallback(
|
sessionPlayerConnector.registerPlayerCallback(executor, callback);
|
||||||
executor, new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch));
|
|
||||||
|
|
||||||
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
|
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
|
||||||
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(0);
|
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(0);
|
||||||
assertThat(onCurrentMediaItemChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS))
|
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
|
||||||
.isTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -1194,16 +1217,15 @@ public class SessionPlayerConnectorTest {
|
|||||||
int listSize = playlist.size();
|
int listSize = playlist.size();
|
||||||
|
|
||||||
// Any value more than list size + 1, to see repeat mode with the recorded video.
|
// Any value more than list size + 1, to see repeat mode with the recorded video.
|
||||||
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(listSize + 2);
|
|
||||||
CopyOnWriteArrayList<MediaItem> currentMediaItemChanges = new CopyOnWriteArrayList<>();
|
CopyOnWriteArrayList<MediaItem> currentMediaItemChanges = new CopyOnWriteArrayList<>();
|
||||||
PlayerCallbackForPlaylist callback =
|
PlayerCallbackForPlaylist callback =
|
||||||
new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch) {
|
new PlayerCallbackForPlaylist(playlist, listSize + 2) {
|
||||||
@Override
|
@Override
|
||||||
public void onCurrentMediaItemChanged(
|
public void onCurrentMediaItemChanged(
|
||||||
@NonNull SessionPlayer player, @NonNull MediaItem item) {
|
@NonNull SessionPlayer player, @NonNull MediaItem item) {
|
||||||
super.onCurrentMediaItemChanged(player, item);
|
super.onCurrentMediaItemChanged(player, item);
|
||||||
currentMediaItemChanges.add(item);
|
currentMediaItemChanges.add(item);
|
||||||
onCurrentMediaItemChangedLatch.countDown();
|
countDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1224,7 +1246,7 @@ public class SessionPlayerConnectorTest {
|
|||||||
assertWithMessage(
|
assertWithMessage(
|
||||||
"Current media item didn't change as expected. Actual changes were %s",
|
"Current media item didn't change as expected. Actual changes were %s",
|
||||||
currentMediaItemChanges)
|
currentMediaItemChanges)
|
||||||
.that(onCurrentMediaItemChangedLatch.await(PLAYBACK_COMPLETED_WAIT_TIME_MS, MILLISECONDS))
|
.that(callback.await(PLAYBACK_COMPLETED_WAIT_TIME_MS, MILLISECONDS))
|
||||||
.isTrue();
|
.isTrue();
|
||||||
|
|
||||||
int expectedMediaItemIndex = 0;
|
int expectedMediaItemIndex = 0;
|
||||||
@ -1286,9 +1308,9 @@ public class SessionPlayerConnectorTest {
|
|||||||
private List<MediaItem> playlist;
|
private List<MediaItem> playlist;
|
||||||
private CountDownLatch onCurrentMediaItemChangedLatch;
|
private CountDownLatch onCurrentMediaItemChangedLatch;
|
||||||
|
|
||||||
PlayerCallbackForPlaylist(List<MediaItem> playlist, CountDownLatch latch) {
|
PlayerCallbackForPlaylist(List<MediaItem> playlist, int count) {
|
||||||
this.playlist = playlist;
|
this.playlist = playlist;
|
||||||
onCurrentMediaItemChangedLatch = latch;
|
onCurrentMediaItemChangedLatch = new CountDownLatch(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1297,5 +1319,17 @@ public class SessionPlayerConnectorTest {
|
|||||||
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(currentIndex);
|
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(currentIndex);
|
||||||
onCurrentMediaItemChangedLatch.countDown();
|
onCurrentMediaItemChangedLatch.countDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void resetLatch(int count) {
|
||||||
|
onCurrentMediaItemChangedLatch = new CountDownLatch(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
|
||||||
|
return onCurrentMediaItemChangedLatch.await(timeout, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void countDown() {
|
||||||
|
onCurrentMediaItemChangedLatch.countDown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +65,10 @@ import java.util.List;
|
|||||||
/** Called when a seek request has completed. */
|
/** Called when a seek request has completed. */
|
||||||
void onSeekCompleted();
|
void onSeekCompleted();
|
||||||
|
|
||||||
/** Called when the player rebuffers. */
|
/** Called when the player starts buffering. */
|
||||||
void onBufferingStarted(androidx.media2.common.MediaItem media2MediaItem);
|
void onBufferingStarted(androidx.media2.common.MediaItem media2MediaItem);
|
||||||
|
|
||||||
/** Called when the player becomes ready again after rebuffering. */
|
/** Called when the player becomes ready again after buffering started. */
|
||||||
void onBufferingEnded(
|
void onBufferingEnded(
|
||||||
androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
|
androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
|
||||||
|
|
||||||
@ -118,8 +118,9 @@ import java.util.List;
|
|||||||
private final List<MediaItem> exoPlayerPlaylist;
|
private final List<MediaItem> exoPlayerPlaylist;
|
||||||
|
|
||||||
private ControlDispatcher controlDispatcher;
|
private ControlDispatcher controlDispatcher;
|
||||||
|
private int sessionPlayerState;
|
||||||
private boolean prepared;
|
private boolean prepared;
|
||||||
private boolean rebuffering;
|
@Nullable private androidx.media2.common.MediaItem bufferingItem;
|
||||||
private int currentWindowIndex;
|
private int currentWindowIndex;
|
||||||
private boolean ignoreTimelineUpdates;
|
private boolean ignoreTimelineUpdates;
|
||||||
|
|
||||||
@ -149,11 +150,14 @@ import java.util.List;
|
|||||||
media2Playlist = new ArrayList<>();
|
media2Playlist = new ArrayList<>();
|
||||||
exoPlayerPlaylist = new ArrayList<>();
|
exoPlayerPlaylist = new ArrayList<>();
|
||||||
currentWindowIndex = C.INDEX_UNSET;
|
currentWindowIndex = C.INDEX_UNSET;
|
||||||
|
|
||||||
prepared = player.getPlaybackState() != Player.STATE_IDLE;
|
|
||||||
rebuffering = player.getPlaybackState() == Player.STATE_BUFFERING;
|
|
||||||
|
|
||||||
updatePlaylist(player.getCurrentTimeline());
|
updatePlaylist(player.getCurrentTimeline());
|
||||||
|
|
||||||
|
sessionPlayerState = evaluateSessionPlayerState();
|
||||||
|
@Player.State int playbackState = player.getPlaybackState();
|
||||||
|
prepared = playbackState != Player.STATE_IDLE;
|
||||||
|
if (playbackState == Player.STATE_BUFFERING) {
|
||||||
|
bufferingItem = getCurrentMediaItem();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
|
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
|
||||||
@ -198,6 +202,9 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean removePlaylistItem(@IntRange(from = 0) int index) {
|
public boolean removePlaylistItem(@IntRange(from = 0) int index) {
|
||||||
|
if (player.getMediaItemCount() <= index) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
player.removeMediaItem(index);
|
player.removeMediaItem(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -353,7 +360,7 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* @SessionPlayer.PlayerState */
|
/* @SessionPlayer.PlayerState */
|
||||||
private int getState() {
|
private int evaluateSessionPlayerState() {
|
||||||
if (hasError()) {
|
if (hasError()) {
|
||||||
return SessionPlayer.PLAYER_STATE_ERROR;
|
return SessionPlayer.PLAYER_STATE_ERROR;
|
||||||
}
|
}
|
||||||
@ -363,7 +370,9 @@ import java.util.List;
|
|||||||
case Player.STATE_IDLE:
|
case Player.STATE_IDLE:
|
||||||
return SessionPlayer.PLAYER_STATE_IDLE;
|
return SessionPlayer.PLAYER_STATE_IDLE;
|
||||||
case Player.STATE_ENDED:
|
case Player.STATE_ENDED:
|
||||||
return SessionPlayer.PLAYER_STATE_PAUSED;
|
return player.getCurrentMediaItem() == null
|
||||||
|
? SessionPlayer.PLAYER_STATE_IDLE
|
||||||
|
: SessionPlayer.PLAYER_STATE_PAUSED;
|
||||||
case Player.STATE_BUFFERING:
|
case Player.STATE_BUFFERING:
|
||||||
case Player.STATE_READY:
|
case Player.STATE_READY:
|
||||||
return playWhenReady
|
return playWhenReady
|
||||||
@ -374,6 +383,65 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateSessionPlayerState() {
|
||||||
|
int newState = evaluateSessionPlayerState();
|
||||||
|
if (sessionPlayerState != newState) {
|
||||||
|
sessionPlayerState = newState;
|
||||||
|
listener.onPlayerStateChanged(newState);
|
||||||
|
if (newState == SessionPlayer.PLAYER_STATE_ERROR) {
|
||||||
|
listener.onError(getCurrentMediaItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBufferingState(boolean isBuffering) {
|
||||||
|
if (isBuffering) {
|
||||||
|
androidx.media2.common.MediaItem curMediaItem = getCurrentMediaItem();
|
||||||
|
if (prepared && (bufferingItem == null || !bufferingItem.equals(curMediaItem))) {
|
||||||
|
bufferingItem = getCurrentMediaItem();
|
||||||
|
listener.onBufferingStarted(Assertions.checkNotNull(bufferingItem));
|
||||||
|
}
|
||||||
|
} else if (bufferingItem != null) {
|
||||||
|
listener.onBufferingEnded(bufferingItem, player.getBufferedPercentage());
|
||||||
|
bufferingItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlayerStateChanged() {
|
||||||
|
updateSessionPlayerState();
|
||||||
|
|
||||||
|
int playbackState = player.getPlaybackState();
|
||||||
|
handler.removeCallbacks(pollBufferRunnable);
|
||||||
|
|
||||||
|
switch (playbackState) {
|
||||||
|
case Player.STATE_IDLE:
|
||||||
|
prepared = false;
|
||||||
|
updateBufferingState(/* isBuffering= */ false);
|
||||||
|
break;
|
||||||
|
case Player.STATE_BUFFERING:
|
||||||
|
updateBufferingState(/* isBuffering= */ true);
|
||||||
|
postOrRun(handler, pollBufferRunnable);
|
||||||
|
break;
|
||||||
|
case Player.STATE_READY:
|
||||||
|
if (!prepared) {
|
||||||
|
prepared = true;
|
||||||
|
handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
||||||
|
listener.onPrepared(
|
||||||
|
Assertions.checkNotNull(getCurrentMediaItem()), player.getBufferedPercentage());
|
||||||
|
}
|
||||||
|
updateBufferingState(/* isBuffering= */ false);
|
||||||
|
postOrRun(handler, pollBufferRunnable);
|
||||||
|
break;
|
||||||
|
case Player.STATE_ENDED:
|
||||||
|
if (player.getCurrentMediaItem() != null) {
|
||||||
|
listener.onPlaybackEnded();
|
||||||
|
}
|
||||||
|
player.setPlayWhenReady(false);
|
||||||
|
updateBufferingState(/* isBuffering= */ false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setAudioAttributes(AudioAttributesCompat audioAttributes) {
|
public void setAudioAttributes(AudioAttributesCompat audioAttributes) {
|
||||||
Player.AudioComponent audioComponent = Assertions.checkStateNotNull(player.getAudioComponent());
|
Player.AudioComponent audioComponent = Assertions.checkStateNotNull(player.getAudioComponent());
|
||||||
audioComponent.setAudioAttributes(
|
audioComponent.setAudioAttributes(
|
||||||
@ -397,7 +465,7 @@ import java.util.List;
|
|||||||
public void reset() {
|
public void reset() {
|
||||||
controlDispatcher.dispatchStop(player, /* reset= */ true);
|
controlDispatcher.dispatchStop(player, /* reset= */ true);
|
||||||
prepared = false;
|
prepared = false;
|
||||||
rebuffering = false;
|
bufferingItem = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
@ -433,35 +501,6 @@ import java.util.List;
|
|||||||
return player.getPlayerError() != null;
|
return player.getPlayerError() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePlayWhenReadyChanged() {
|
|
||||||
listener.onPlayerStateChanged(getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePlayerStateChanged(@Player.State int state) {
|
|
||||||
if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) {
|
|
||||||
postOrRun(handler, pollBufferRunnable);
|
|
||||||
} else {
|
|
||||||
handler.removeCallbacks(pollBufferRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case Player.STATE_BUFFERING:
|
|
||||||
maybeNotifyBufferingEvents();
|
|
||||||
break;
|
|
||||||
case Player.STATE_READY:
|
|
||||||
maybeNotifyReadyEvents();
|
|
||||||
break;
|
|
||||||
case Player.STATE_ENDED:
|
|
||||||
maybeNotifyEndedEvents();
|
|
||||||
break;
|
|
||||||
case Player.STATE_IDLE:
|
|
||||||
// Do nothing.
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
private void handlePositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||||
int currentWindowIndex = getCurrentMediaItemIndex();
|
int currentWindowIndex = getCurrentMediaItemIndex();
|
||||||
if (this.currentWindowIndex != currentWindowIndex) {
|
if (this.currentWindowIndex != currentWindowIndex) {
|
||||||
@ -474,34 +513,6 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePlayerError() {
|
|
||||||
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_ERROR);
|
|
||||||
listener.onError(getCurrentMediaItem());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
|
||||||
listener.onRepeatModeChanged(Utils.getRepeatMode(repeatMode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleShuffleMode(boolean shuffleModeEnabled) {
|
|
||||||
listener.onShuffleModeChanged(Utils.getShuffleMode(shuffleModeEnabled));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
|
||||||
listener.onPlaybackSpeedChanged(playbackParameters.speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleTimelineChanged(Timeline timeline) {
|
|
||||||
if (ignoreTimelineUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isExoPlayerMediaItemsChanged(timeline)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updatePlaylist(timeline);
|
|
||||||
listener.onPlaylistChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether Timeline is changed by media item changes or not
|
// Check whether Timeline is changed by media item changes or not
|
||||||
private boolean isExoPlayerMediaItemsChanged(Timeline timeline) {
|
private boolean isExoPlayerMediaItemsChanged(Timeline timeline) {
|
||||||
if (exoPlayerPlaylist.size() != timeline.getWindowCount()) {
|
if (exoPlayerPlaylist.size() != timeline.getWindowCount()) {
|
||||||
@ -541,10 +552,6 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAudioAttributesChanged(AudioAttributes audioAttributes) {
|
|
||||||
listener.onAudioAttributesChanged(Utils.getAudioAttributesCompat(audioAttributes));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateBufferingAndScheduleNextPollBuffer() {
|
private void updateBufferingAndScheduleNextPollBuffer() {
|
||||||
androidx.media2.common.MediaItem media2MediaItem =
|
androidx.media2.common.MediaItem media2MediaItem =
|
||||||
Assertions.checkNotNull(getCurrentMediaItem());
|
Assertions.checkNotNull(getCurrentMediaItem());
|
||||||
@ -553,39 +560,6 @@ import java.util.List;
|
|||||||
handler.postDelayed(pollBufferRunnable, POLL_BUFFER_INTERVAL_MS);
|
handler.postDelayed(pollBufferRunnable, POLL_BUFFER_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeNotifyBufferingEvents() {
|
|
||||||
androidx.media2.common.MediaItem media2MediaItem =
|
|
||||||
Assertions.checkNotNull(getCurrentMediaItem());
|
|
||||||
if (prepared && !rebuffering) {
|
|
||||||
rebuffering = true;
|
|
||||||
listener.onBufferingStarted(media2MediaItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeNotifyReadyEvents() {
|
|
||||||
androidx.media2.common.MediaItem media2MediaItem =
|
|
||||||
Assertions.checkNotNull(getCurrentMediaItem());
|
|
||||||
boolean prepareComplete = !prepared;
|
|
||||||
if (prepareComplete) {
|
|
||||||
prepared = true;
|
|
||||||
handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
|
|
||||||
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED);
|
|
||||||
listener.onPrepared(media2MediaItem, player.getBufferedPercentage());
|
|
||||||
}
|
|
||||||
if (rebuffering) {
|
|
||||||
rebuffering = false;
|
|
||||||
listener.onBufferingEnded(media2MediaItem, player.getBufferedPercentage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void maybeNotifyEndedEvents() {
|
|
||||||
if (player.getPlayWhenReady()) {
|
|
||||||
listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED);
|
|
||||||
listener.onPlaybackEnded();
|
|
||||||
player.setPlayWhenReady(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
|
private void releaseMediaItem(androidx.media2.common.MediaItem media2MediaItem) {
|
||||||
try {
|
try {
|
||||||
if (media2MediaItem instanceof CallbackMediaItem) {
|
if (media2MediaItem instanceof CallbackMediaItem) {
|
||||||
@ -602,12 +576,12 @@ import java.util.List;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||||
handlePlayWhenReadyChanged();
|
updateSessionPlayerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackStateChanged(@Player.State int state) {
|
public void onPlaybackStateChanged(@Player.State int state) {
|
||||||
handlePlayerStateChanged(state);
|
handlePlayerStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -617,34 +591,41 @@ import java.util.List;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerError(ExoPlaybackException error) {
|
public void onPlayerError(ExoPlaybackException error) {
|
||||||
handlePlayerError();
|
updateSessionPlayerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
|
||||||
handleRepeatModeChanged(repeatMode);
|
listener.onRepeatModeChanged(Utils.getRepeatMode(repeatMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||||
handleShuffleMode(shuffleModeEnabled);
|
listener.onShuffleModeChanged(Utils.getShuffleMode(shuffleModeEnabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||||
handlePlaybackParametersChanged(playbackParameters);
|
listener.onPlaybackSpeedChanged(playbackParameters.speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||||
handleTimelineChanged(timeline);
|
if (ignoreTimelineUpdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isExoPlayerMediaItemsChanged(timeline)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updatePlaylist(timeline);
|
||||||
|
listener.onPlaylistChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioListener implementation.
|
// AudioListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
|
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
|
||||||
handleAudioAttributesChanged(audioAttributes);
|
listener.onAudioAttributesChanged(Utils.getAudioAttributesCompat(audioAttributes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -559,12 +559,16 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this suppress warnings and call onCurrentMediaItemChanged with a null item
|
||||||
|
// once AndroidX media2 1.2.0 is released
|
||||||
|
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||||
private void handlePlaylistChangedOnHandler() {
|
private void handlePlaylistChangedOnHandler() {
|
||||||
List<MediaItem> currentPlaylist = player.getPlaylist();
|
List<MediaItem> currentPlaylist = player.getPlaylist();
|
||||||
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
|
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
|
||||||
|
|
||||||
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||||
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
|
boolean notifyCurrentMediaItem =
|
||||||
|
!ObjectsCompat.equals(this.currentMediaItem, currentMediaItem) && currentMediaItem != null;
|
||||||
this.currentMediaItem = currentMediaItem;
|
this.currentMediaItem = currentMediaItem;
|
||||||
|
|
||||||
long currentPosition = getCurrentPosition();
|
long currentPosition = getCurrentPosition();
|
||||||
@ -573,9 +577,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
|
|||||||
callback.onPlaylistChanged(
|
callback.onPlaylistChanged(
|
||||||
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
|
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
|
||||||
if (notifyCurrentMediaItem) {
|
if (notifyCurrentMediaItem) {
|
||||||
Assertions.checkNotNull(
|
|
||||||
currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null");
|
|
||||||
|
|
||||||
callback.onCurrentMediaItemChanged(SessionPlayerConnector.this, currentMediaItem);
|
callback.onCurrentMediaItemChanged(SessionPlayerConnector.this, currentMediaItem);
|
||||||
|
|
||||||
// Workaround for MediaSession's issue that current media item change isn't propagated
|
// Workaround for MediaSession's issue that current media item change isn't propagated
|
||||||
|
@ -1147,6 +1147,8 @@ public final class MediaSessionConnector {
|
|||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
playbackPreparer.onPrepare(/* playWhenReady= */ true);
|
playbackPreparer.onPrepare(/* playWhenReady= */ true);
|
||||||
|
} else {
|
||||||
|
controlDispatcher.dispatchPrepare(player);
|
||||||
}
|
}
|
||||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||||
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||||
|
@ -242,6 +242,11 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
|||||||
responseBody = Assertions.checkNotNull(response.body());
|
responseBody = Assertions.checkNotNull(response.body());
|
||||||
responseByteStream = responseBody.byteStream();
|
responseByteStream = responseBody.byteStream();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@Nullable String message = e.getMessage();
|
||||||
|
if (message != null
|
||||||
|
&& Util.toLowerInvariant(message).matches("cleartext communication.*not permitted.*")) {
|
||||||
|
throw new CleartextNotPermittedException(e, dataSpec);
|
||||||
|
}
|
||||||
throw new HttpDataSourceException(
|
throw new HttpDataSourceException(
|
||||||
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||||
}
|
}
|
||||||
|
@ -7,3 +7,12 @@
|
|||||||
|
|
||||||
# From https://github.com/google/guava/wiki/UsingProGuardWithGuava
|
# From https://github.com/google/guava/wiki/UsingProGuardWithGuava
|
||||||
-dontwarn java.lang.ClassValue
|
-dontwarn java.lang.ClassValue
|
||||||
|
-dontwarn java.lang.SafeVarargs
|
||||||
|
-dontwarn javax.lang.model.element.Modifier
|
||||||
|
-dontwarn sun.misc.Unsafe
|
||||||
|
|
||||||
|
# Don't warn about Guava's compile-only dependencies.
|
||||||
|
# These lines are needed for ProGuard but not R8.
|
||||||
|
-dontwarn com.google.errorprone.annotations.**
|
||||||
|
-dontwarn com.google.j2objc.annotations.**
|
||||||
|
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||||
|
@ -253,8 +253,7 @@ public final class C {
|
|||||||
/**
|
/**
|
||||||
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
|
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
|
||||||
* #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
|
* #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
|
||||||
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link
|
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM} or {@link #STREAM_TYPE_VOICE_CALL}.
|
||||||
* #STREAM_TYPE_USE_DEFAULT}.
|
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@ -265,8 +264,7 @@ public final class C {
|
|||||||
STREAM_TYPE_NOTIFICATION,
|
STREAM_TYPE_NOTIFICATION,
|
||||||
STREAM_TYPE_RING,
|
STREAM_TYPE_RING,
|
||||||
STREAM_TYPE_SYSTEM,
|
STREAM_TYPE_SYSTEM,
|
||||||
STREAM_TYPE_VOICE_CALL,
|
STREAM_TYPE_VOICE_CALL
|
||||||
STREAM_TYPE_USE_DEFAULT
|
|
||||||
})
|
})
|
||||||
public @interface StreamType {}
|
public @interface StreamType {}
|
||||||
/**
|
/**
|
||||||
@ -297,13 +295,7 @@ public final class C {
|
|||||||
* @see AudioManager#STREAM_VOICE_CALL
|
* @see AudioManager#STREAM_VOICE_CALL
|
||||||
*/
|
*/
|
||||||
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
|
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
|
||||||
/**
|
/** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */
|
||||||
* @see AudioManager#USE_DEFAULT_STREAM_TYPE
|
|
||||||
*/
|
|
||||||
public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE;
|
|
||||||
/**
|
|
||||||
* The default stream type used by audio renderers.
|
|
||||||
*/
|
|
||||||
public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
|
public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
|
|
||||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||||
public static final String VERSION = "2.12.1";
|
public static final String VERSION = "2.12.2";
|
||||||
|
|
||||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.1";
|
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library expressed as an integer, for example 1002003.
|
* The version of the library expressed as an integer, for example 1002003.
|
||||||
@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
* integer version 123045006 (123-045-006).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final int VERSION_INT = 2012001;
|
public static final int VERSION_INT = 2012002;
|
||||||
|
|
||||||
/** The default user agent for requests made by the library. */
|
/** The default user agent for requests made by the library. */
|
||||||
public static final String DEFAULT_USER_AGENT =
|
public static final String DEFAULT_USER_AGENT =
|
||||||
|
@ -216,7 +216,7 @@ public final class MediaItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the optional DRM license server URI. If this URI is set, the {@link
|
* Sets the optional default DRM license server URI. If this URI is set, the {@link
|
||||||
* DrmConfiguration#uuid} needs to be specified as well.
|
* DrmConfiguration#uuid} needs to be specified as well.
|
||||||
*
|
*
|
||||||
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
|
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
|
||||||
@ -228,7 +228,7 @@ public final class MediaItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the optional DRM license server URI. If this URI is set, the {@link
|
* Sets the optional default DRM license server URI. If this URI is set, the {@link
|
||||||
* DrmConfiguration#uuid} needs to be specified as well.
|
* DrmConfiguration#uuid} needs to be specified as well.
|
||||||
*
|
*
|
||||||
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
|
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
|
||||||
@ -279,8 +279,8 @@ public final class MediaItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether to use the DRM license server URI of the media item for key requests that
|
* Sets whether to force use the default DRM license server URI even if the media specifies its
|
||||||
* include their own DRM license server URI.
|
* own DRM license server URI.
|
||||||
*
|
*
|
||||||
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM force default license flag is
|
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM force default license flag is
|
||||||
* used to create a {@link PlaybackProperties} object. Otherwise it will be ignored.
|
* used to create a {@link PlaybackProperties} object. Otherwise it will be ignored.
|
||||||
@ -482,8 +482,8 @@ public final class MediaItem {
|
|||||||
public final UUID uuid;
|
public final UUID uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional DRM license server {@link Uri}. If {@code null} then the DRM license server must be
|
* Optional default DRM license server {@link Uri}. If {@code null} then the DRM license server
|
||||||
* specified by the media.
|
* must be specified by the media.
|
||||||
*/
|
*/
|
||||||
@Nullable public final Uri licenseUri;
|
@Nullable public final Uri licenseUri;
|
||||||
|
|
||||||
@ -500,8 +500,8 @@ public final class MediaItem {
|
|||||||
public final boolean playClearContentWithoutKey;
|
public final boolean playClearContentWithoutKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether to use the DRM license server URI of the media item for key requests that
|
* Whether to force use of {@link #licenseUri} even if the media specifies its own DRM license
|
||||||
* include their own DRM license server URI.
|
* server URI.
|
||||||
*/
|
*/
|
||||||
public final boolean forceDefaultLicenseUri;
|
public final boolean forceDefaultLicenseUri;
|
||||||
|
|
||||||
@ -519,6 +519,7 @@ public final class MediaItem {
|
|||||||
boolean playClearContentWithoutKey,
|
boolean playClearContentWithoutKey,
|
||||||
List<Integer> drmSessionForClearTypes,
|
List<Integer> drmSessionForClearTypes,
|
||||||
@Nullable byte[] keySetId) {
|
@Nullable byte[] keySetId) {
|
||||||
|
Assertions.checkArgument(!(forceDefaultLicenseUri && licenseUri == null));
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.licenseUri = licenseUri;
|
this.licenseUri = licenseUri;
|
||||||
this.requestHeaders = requestHeaders;
|
this.requestHeaders = requestHeaders;
|
||||||
|
@ -271,7 +271,24 @@ public interface HttpDataSource extends DataSource {
|
|||||||
this.dataSpec = dataSpec;
|
this.dataSpec = dataSpec;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when cleartext HTTP traffic is not permitted. For more information including how to
|
||||||
|
* enable cleartext traffic, see the <a
|
||||||
|
* href="https://exoplayer.dev/issues/cleartext-not-permitted">corresponding troubleshooting
|
||||||
|
* topic</a>.
|
||||||
|
*/
|
||||||
|
final class CleartextNotPermittedException extends HttpDataSourceException {
|
||||||
|
|
||||||
|
public CleartextNotPermittedException(IOException cause, DataSpec dataSpec) {
|
||||||
|
super(
|
||||||
|
"Cleartext HTTP traffic not permitted. See"
|
||||||
|
+ " https://exoplayer.dev/issues/cleartext-not-permitted",
|
||||||
|
cause,
|
||||||
|
dataSpec,
|
||||||
|
TYPE_OPEN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -285,7 +302,6 @@ public interface HttpDataSource extends DataSource {
|
|||||||
super("Invalid content type: " + contentType, dataSpec, TYPE_OPEN);
|
super("Invalid content type: " + contentType, dataSpec, TYPE_OPEN);
|
||||||
this.contentType = contentType;
|
this.contentType = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -239,6 +239,50 @@ public final class MimeTypes {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given {@code codecs} string contains a codec which corresponds to the given
|
||||||
|
* {@code mimeType}.
|
||||||
|
*
|
||||||
|
* @param codecs An RFC 6381 codecs string.
|
||||||
|
* @param mimeType A MIME type to look for.
|
||||||
|
* @return Whether the given {@code codecs} string contains a codec which corresponds to the given
|
||||||
|
* {@code mimeType}.
|
||||||
|
*/
|
||||||
|
public static boolean containsCodecsCorrespondingToMimeType(
|
||||||
|
@Nullable String codecs, String mimeType) {
|
||||||
|
return getCodecsCorrespondingToMimeType(codecs, mimeType) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a subsequence of {@code codecs} containing the codec strings that correspond to the
|
||||||
|
* given {@code mimeType}. Returns null if {@code mimeType} is null, {@code codecs} is null, or
|
||||||
|
* {@code codecs} does not contain a codec that corresponds to {@code mimeType}.
|
||||||
|
*
|
||||||
|
* @param codecs An RFC 6381 codecs string.
|
||||||
|
* @param mimeType A MIME type to look for.
|
||||||
|
* @return A subsequence of {@code codecs} containing the codec strings that correspond to the
|
||||||
|
* given {@code mimeType}. Returns null if {@code mimeType} is null, {@code codecs} is null,
|
||||||
|
* or {@code codecs} does not contain a codec that corresponds to {@code mimeType}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static String getCodecsCorrespondingToMimeType(
|
||||||
|
@Nullable String codecs, @Nullable String mimeType) {
|
||||||
|
if (codecs == null || mimeType == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] codecList = Util.splitCodecs(codecs);
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (String codec : codecList) {
|
||||||
|
if (mimeType.equals(getMediaMimeType(codec))) {
|
||||||
|
if (builder.length() > 0) {
|
||||||
|
builder.append(",");
|
||||||
|
}
|
||||||
|
builder.append(codec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.length() > 0 ? builder.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the first audio MIME type derived from an RFC 6381 codecs string.
|
* Returns the first audio MIME type derived from an RFC 6381 codecs string.
|
||||||
*
|
*
|
||||||
|
@ -515,8 +515,8 @@ public final class ParsableByteArray {
|
|||||||
* Reads a line of text.
|
* Reads a line of text.
|
||||||
*
|
*
|
||||||
* <p>A line is considered to be terminated by any one of a carriage return ('\r'), a line feed
|
* <p>A line is considered to be terminated by any one of a carriage return ('\r'), a line feed
|
||||||
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default
|
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The UTF-8 charset is
|
||||||
* charset (UTF-8) is used. This method discards leading UTF-8 byte order marks, if present.
|
* used. This method discards leading UTF-8 byte order marks, if present.
|
||||||
*
|
*
|
||||||
* @return The line not including any line-termination characters, or null if the end of the data
|
* @return The line not including any line-termination characters, or null if the end of the data
|
||||||
* has already been reached.
|
* has already been reached.
|
||||||
|
@ -1485,6 +1485,18 @@ public final class Util {
|
|||||||
+ ") " + ExoPlayerLibraryInfo.VERSION_SLASHY;
|
+ ") " + ExoPlayerLibraryInfo.VERSION_SLASHY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the number of codec strings in {@code codecs} whose type matches {@code trackType}. */
|
||||||
|
public static int getCodecCountOfType(@Nullable String codecs, int trackType) {
|
||||||
|
String[] codecArray = splitCodecs(codecs);
|
||||||
|
int count = 0;
|
||||||
|
for (String codec : codecArray) {
|
||||||
|
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code
|
* Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code
|
||||||
* trackType}.
|
* trackType}.
|
||||||
@ -1677,7 +1689,6 @@ public final class Util {
|
|||||||
return C.USAGE_ASSISTANCE_SONIFICATION;
|
return C.USAGE_ASSISTANCE_SONIFICATION;
|
||||||
case C.STREAM_TYPE_VOICE_CALL:
|
case C.STREAM_TYPE_VOICE_CALL:
|
||||||
return C.USAGE_VOICE_COMMUNICATION;
|
return C.USAGE_VOICE_COMMUNICATION;
|
||||||
case C.STREAM_TYPE_USE_DEFAULT:
|
|
||||||
case C.STREAM_TYPE_MUSIC:
|
case C.STREAM_TYPE_MUSIC:
|
||||||
default:
|
default:
|
||||||
return C.USAGE_MEDIA;
|
return C.USAGE_MEDIA;
|
||||||
@ -1698,7 +1709,6 @@ public final class Util {
|
|||||||
return C.CONTENT_TYPE_SONIFICATION;
|
return C.CONTENT_TYPE_SONIFICATION;
|
||||||
case C.STREAM_TYPE_VOICE_CALL:
|
case C.STREAM_TYPE_VOICE_CALL:
|
||||||
return C.CONTENT_TYPE_SPEECH;
|
return C.CONTENT_TYPE_SPEECH;
|
||||||
case C.STREAM_TYPE_USE_DEFAULT:
|
|
||||||
case C.STREAM_TYPE_MUSIC:
|
case C.STREAM_TYPE_MUSIC:
|
||||||
default:
|
default:
|
||||||
return C.CONTENT_TYPE_MUSIC;
|
return C.CONTENT_TYPE_MUSIC;
|
||||||
|
@ -28,6 +28,69 @@ import org.junit.runner.RunWith;
|
|||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class MimeTypesTest {
|
public final class MimeTypesTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void containsCodecsCorrespondingToMimeType_returnsCorrectResult() {
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.containsCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AAC))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.containsCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.containsCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.VIDEO_H264))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.containsCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "unknown-codec,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AAC))
|
||||||
|
.isTrue();
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.containsCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "unknown-codec,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.containsCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ null, MimeTypes.AUDIO_AC3))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.containsCodecsCorrespondingToMimeType(/* codecs= */ "", MimeTypes.AUDIO_AC3))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getCodecsCorrespondingToMimeType_returnsCorrectResult() {
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.getCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AAC))
|
||||||
|
.isEqualTo("mp4a.40.2");
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.getCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.VIDEO_H264))
|
||||||
|
.isEqualTo("avc1.4D5015,avc1.4D4015");
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.getCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
|
||||||
|
.isEqualTo("ac-3");
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.getCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "unknown-codec,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
|
||||||
|
.isEqualTo("ac-3");
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.getCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.VIDEO_H265))
|
||||||
|
.isNull();
|
||||||
|
assertThat(
|
||||||
|
MimeTypes.getCodecsCorrespondingToMimeType(
|
||||||
|
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", null))
|
||||||
|
.isNull();
|
||||||
|
assertThat(MimeTypes.getCodecsCorrespondingToMimeType(/* codecs= */ null, MimeTypes.AUDIO_AAC))
|
||||||
|
.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void isText_returnsCorrectResult() {
|
public void isText_returnsCorrectResult() {
|
||||||
assertThat(MimeTypes.isText(MimeTypes.TEXT_VTT)).isTrue();
|
assertThat(MimeTypes.isText(MimeTypes.TEXT_VTT)).isTrue();
|
||||||
|
@ -26,6 +26,14 @@ import com.google.android.exoplayer2.Player.RepeatMode;
|
|||||||
*/
|
*/
|
||||||
public interface ControlDispatcher {
|
public interface ControlDispatcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a {@link Player#prepare()} operation.
|
||||||
|
*
|
||||||
|
* @param player The {@link Player} to which the operation should be dispatched.
|
||||||
|
* @return True if the operation was dispatched. False if suppressed.
|
||||||
|
*/
|
||||||
|
boolean dispatchPrepare(Player player);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches a {@link Player#setPlayWhenReady(boolean)} operation.
|
* Dispatches a {@link Player#setPlayWhenReady(boolean)} operation.
|
||||||
*
|
*
|
||||||
|
@ -52,6 +52,12 @@ public class DefaultControlDispatcher implements ControlDispatcher {
|
|||||||
window = new Timeline.Window();
|
window = new Timeline.Window();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchPrepare(Player player) {
|
||||||
|
player.prepare();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
|
public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
|
||||||
player.setPlayWhenReady(playWhenReady);
|
player.setPlayWhenReady(playWhenReady);
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
/** Called to prepare a playback. */
|
/** @deprecated Use {@link ControlDispatcher} instead. */
|
||||||
|
@Deprecated
|
||||||
public interface PlaybackPreparer {
|
public interface PlaybackPreparer {
|
||||||
|
|
||||||
/** Called to prepare a playback. */
|
/** @deprecated Use {@link ControlDispatcher#dispatchPrepare(Player)} instead. */
|
||||||
|
@Deprecated
|
||||||
void preparePlayback();
|
void preparePlayback();
|
||||||
}
|
}
|
||||||
|
@ -722,15 +722,21 @@ public interface Player {
|
|||||||
@IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
|
@IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
|
||||||
@interface RepeatMode {}
|
@interface RepeatMode {}
|
||||||
/**
|
/**
|
||||||
* Normal playback without repetition.
|
* Normal playback without repetition. "Previous" and "Next" actions move to the previous and next
|
||||||
|
* windows respectively, and do nothing when there is no previous or next window to move to.
|
||||||
*/
|
*/
|
||||||
int REPEAT_MODE_OFF = 0;
|
int REPEAT_MODE_OFF = 0;
|
||||||
/**
|
/**
|
||||||
* "Repeat One" mode to repeat the currently playing window infinitely.
|
* Repeats the currently playing window infinitely during ongoing playback. "Previous" and "Next"
|
||||||
|
* actions behave as they do in {@link #REPEAT_MODE_OFF}, moving to the previous and next windows
|
||||||
|
* respectively, and doing nothing when there is no previous or next window to move to.
|
||||||
*/
|
*/
|
||||||
int REPEAT_MODE_ONE = 1;
|
int REPEAT_MODE_ONE = 1;
|
||||||
/**
|
/**
|
||||||
* "Repeat All" mode to repeat the entire timeline infinitely.
|
* Repeats the entire timeline infinitely. "Previous" and "Next" actions behave as they do in
|
||||||
|
* {@link #REPEAT_MODE_OFF}, but with looping at the ends so that "Previous" when playing the
|
||||||
|
* first window will move to the last window, and "Next" when playing the last window will move to
|
||||||
|
* the first window.
|
||||||
*/
|
*/
|
||||||
int REPEAT_MODE_ALL = 2;
|
int REPEAT_MODE_ALL = 2;
|
||||||
|
|
||||||
@ -1126,26 +1132,41 @@ public interface Player {
|
|||||||
/**
|
/**
|
||||||
* Returns whether a previous window exists, which may depend on the current repeat mode and
|
* Returns whether a previous window exists, which may depend on the current repeat mode and
|
||||||
* whether shuffle mode is enabled.
|
* whether shuffle mode is enabled.
|
||||||
|
*
|
||||||
|
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
|
||||||
|
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
|
||||||
|
* details.
|
||||||
*/
|
*/
|
||||||
boolean hasPrevious();
|
boolean hasPrevious();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to the default position of the previous window in the timeline, which may depend on the
|
* Seeks to the default position of the previous window, which may depend on the current repeat
|
||||||
* current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()}
|
* mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()} is {@code
|
||||||
* is {@code false}.
|
* false}.
|
||||||
|
*
|
||||||
|
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
|
||||||
|
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
|
||||||
|
* details.
|
||||||
*/
|
*/
|
||||||
void previous();
|
void previous();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether a next window exists, which may depend on the current repeat mode and whether
|
* Returns whether a next window exists, which may depend on the current repeat mode and whether
|
||||||
* shuffle mode is enabled.
|
* shuffle mode is enabled.
|
||||||
|
*
|
||||||
|
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
|
||||||
|
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
|
||||||
|
* details.
|
||||||
*/
|
*/
|
||||||
boolean hasNext();
|
boolean hasNext();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seeks to the default position of the next window in the timeline, which may depend on the
|
* Seeks to the default position of the next window, which may depend on the current repeat mode
|
||||||
* current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is
|
* and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is {@code false}.
|
||||||
* {@code false}.
|
*
|
||||||
|
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
|
||||||
|
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
|
||||||
|
* details.
|
||||||
*/
|
*/
|
||||||
void next();
|
void next();
|
||||||
|
|
||||||
@ -1254,18 +1275,24 @@ public interface Player {
|
|||||||
int getCurrentWindowIndex();
|
int getCurrentWindowIndex();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of the next timeline window to be played, which may depend on the current
|
* Returns the index of the window that will be played if {@link #next()} is called, which may
|
||||||
* repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window
|
* depend on the current repeat mode and whether shuffle mode is enabled. Returns {@link
|
||||||
* currently being played is the last window or if the {@link #getCurrentTimeline() current
|
* C#INDEX_UNSET} if {@link #hasNext()} is {@code false}.
|
||||||
* timeline} is empty.
|
*
|
||||||
|
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
|
||||||
|
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
|
||||||
|
* details.
|
||||||
*/
|
*/
|
||||||
int getNextWindowIndex();
|
int getNextWindowIndex();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of the previous timeline window to be played, which may depend on the current
|
* Returns the index of the window that will be played if {@link #previous()} is called, which may
|
||||||
* repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window
|
* depend on the current repeat mode and whether shuffle mode is enabled. Returns {@link
|
||||||
* currently being played is the first window or if the {@link #getCurrentTimeline() current
|
* C#INDEX_UNSET} if {@link #hasPrevious()} is {@code false}.
|
||||||
* timeline} is empty.
|
*
|
||||||
|
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
|
||||||
|
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
|
||||||
|
* details.
|
||||||
*/
|
*/
|
||||||
int getPreviousWindowIndex();
|
int getPreviousWindowIndex();
|
||||||
|
|
||||||
|
@ -21,7 +21,9 @@ import android.content.Intent;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/** A manager that wraps {@link AudioManager} to control/listen audio stream volume. */
|
/** A manager that wraps {@link AudioManager} to control/listen audio stream volume. */
|
||||||
@ -37,6 +39,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
void onStreamVolumeChanged(int streamVolume, boolean streamMuted);
|
void onStreamVolumeChanged(int streamVolume, boolean streamMuted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "StreamVolumeManager";
|
||||||
|
|
||||||
// TODO(b/151280453): Replace the hidden intent action with an official one.
|
// TODO(b/151280453): Replace the hidden intent action with an official one.
|
||||||
// Copied from AudioManager#VOLUME_CHANGED_ACTION
|
// Copied from AudioManager#VOLUME_CHANGED_ACTION
|
||||||
private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION";
|
private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION";
|
||||||
@ -48,12 +52,11 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
private final Handler eventHandler;
|
private final Handler eventHandler;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final AudioManager audioManager;
|
private final AudioManager audioManager;
|
||||||
private final VolumeChangeReceiver receiver;
|
|
||||||
|
|
||||||
|
@Nullable private VolumeChangeReceiver receiver;
|
||||||
@C.StreamType private int streamType;
|
@C.StreamType private int streamType;
|
||||||
private int volume;
|
private int volume;
|
||||||
private boolean muted;
|
private boolean muted;
|
||||||
private boolean released;
|
|
||||||
|
|
||||||
/** Creates a manager. */
|
/** Creates a manager. */
|
||||||
public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) {
|
public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) {
|
||||||
@ -68,9 +71,14 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
volume = getVolumeFromManager(audioManager, streamType);
|
volume = getVolumeFromManager(audioManager, streamType);
|
||||||
muted = getMutedFromManager(audioManager, streamType);
|
muted = getMutedFromManager(audioManager, streamType);
|
||||||
|
|
||||||
receiver = new VolumeChangeReceiver();
|
VolumeChangeReceiver receiver = new VolumeChangeReceiver();
|
||||||
IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION);
|
IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION);
|
||||||
applicationContext.registerReceiver(receiver, filter);
|
try {
|
||||||
|
applicationContext.registerReceiver(receiver, filter);
|
||||||
|
this.receiver = receiver;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
Log.w(TAG, "Error registering stream volume receiver", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the audio stream type. */
|
/** Sets the audio stream type. */
|
||||||
@ -159,11 +167,14 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
|
|
||||||
/** Releases the manager. It must be called when the manager is no longer required. */
|
/** Releases the manager. It must be called when the manager is no longer required. */
|
||||||
public void release() {
|
public void release() {
|
||||||
if (released) {
|
if (receiver != null) {
|
||||||
return;
|
try {
|
||||||
|
applicationContext.unregisterReceiver(receiver);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
Log.w(TAG, "Error unregistering stream volume receiver", e);
|
||||||
|
}
|
||||||
|
receiver = null;
|
||||||
}
|
}
|
||||||
applicationContext.unregisterReceiver(receiver);
|
|
||||||
released = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVolumeAndNotifyIfChanged() {
|
private void updateVolumeAndNotifyIfChanged() {
|
||||||
@ -177,7 +188,14 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) {
|
private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) {
|
||||||
return audioManager.getStreamVolume(streamType);
|
// AudioManager#getStreamVolume(int) throws an exception on some devices. See
|
||||||
|
// https://github.com/google/ExoPlayer/issues/8191.
|
||||||
|
try {
|
||||||
|
return audioManager.getStreamVolume(streamType);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
Log.w(TAG, "Could not retrieve stream volume for stream type " + streamType, e);
|
||||||
|
return audioManager.getStreamMaxVolume(streamType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean getMutedFromManager(
|
private static boolean getMutedFromManager(
|
||||||
@ -185,7 +203,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
if (Util.SDK_INT >= 23) {
|
if (Util.SDK_INT >= 23) {
|
||||||
return audioManager.isStreamMute(streamType);
|
return audioManager.isStreamMute(streamType);
|
||||||
} else {
|
} else {
|
||||||
return audioManager.getStreamVolume(streamType) == 0;
|
return getVolumeFromManager(audioManager, streamType) == 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,8 +113,15 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
boolean applySkipSilenceEnabled(boolean skipSilenceEnabled);
|
boolean applySkipSilenceEnabled(boolean skipSilenceEnabled);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scales the specified playout duration to take into account speedup due to audio processing,
|
* Returns the media duration corresponding to the specified playout duration, taking speed
|
||||||
* returning an input media duration, in arbitrary units.
|
* adjustment due to audio processing into account.
|
||||||
|
*
|
||||||
|
* <p>The scaling performed by this method will use the actual playback speed achieved by the
|
||||||
|
* audio processor chain, on average, since it was last flushed. This may differ very slightly
|
||||||
|
* from the target playback speed.
|
||||||
|
*
|
||||||
|
* @param playoutDuration The playout duration to scale.
|
||||||
|
* @return The corresponding media duration, in the same units as {@code duration}.
|
||||||
*/
|
*/
|
||||||
long getMediaDuration(long playoutDuration);
|
long getMediaDuration(long playoutDuration);
|
||||||
|
|
||||||
@ -173,9 +180,9 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
|
public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
|
||||||
float speed = sonicAudioProcessor.setSpeed(playbackParameters.speed);
|
sonicAudioProcessor.setSpeed(playbackParameters.speed);
|
||||||
float pitch = sonicAudioProcessor.setPitch(playbackParameters.pitch);
|
sonicAudioProcessor.setPitch(playbackParameters.pitch);
|
||||||
return new PlaybackParameters(speed, pitch);
|
return playbackParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -186,7 +193,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getMediaDuration(long playoutDuration) {
|
public long getMediaDuration(long playoutDuration) {
|
||||||
return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration);
|
return sonicAudioProcessor.getMediaDuration(playoutDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1369,21 +1376,33 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
mediaPositionParameters = mediaPositionParametersCheckpoints.remove();
|
mediaPositionParameters = mediaPositionParametersCheckpoints.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
long playoutDurationSinceLastCheckpoint =
|
long playoutDurationSinceLastCheckpointUs =
|
||||||
positionUs - mediaPositionParameters.audioTrackPositionUs;
|
positionUs - mediaPositionParameters.audioTrackPositionUs;
|
||||||
if (!mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
|
if (mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
|
||||||
if (mediaPositionParametersCheckpoints.isEmpty()) {
|
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpointUs;
|
||||||
playoutDurationSinceLastCheckpoint =
|
} else if (mediaPositionParametersCheckpoints.isEmpty()) {
|
||||||
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpoint);
|
long mediaDurationSinceLastCheckpointUs =
|
||||||
} else {
|
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpointUs);
|
||||||
// Playing data at a previous playback speed, so fall back to multiplying by the speed.
|
return mediaPositionParameters.mediaTimeUs + mediaDurationSinceLastCheckpointUs;
|
||||||
playoutDurationSinceLastCheckpoint =
|
} else {
|
||||||
Util.getMediaDurationForPlayoutDuration(
|
// The processor chain has been configured with new parameters, but we're still playing audio
|
||||||
playoutDurationSinceLastCheckpoint,
|
// that was processed using previous parameters. We can't scale the playout duration using the
|
||||||
mediaPositionParameters.playbackParameters.speed);
|
// processor chain in this case, so we fall back to scaling using the previous parameters'
|
||||||
}
|
// target speed instead. Since the processor chain may not have achieved the target speed
|
||||||
|
// precisely, we scale the duration to the next checkpoint (which will always be small) rather
|
||||||
|
// than the duration from the previous checkpoint (which may be arbitrarily large). This
|
||||||
|
// limits the amount of error that can be introduced due to a difference between the target
|
||||||
|
// and actual speeds.
|
||||||
|
MediaPositionParameters nextMediaPositionParameters =
|
||||||
|
mediaPositionParametersCheckpoints.getFirst();
|
||||||
|
long playoutDurationUntilNextCheckpointUs =
|
||||||
|
nextMediaPositionParameters.audioTrackPositionUs - positionUs;
|
||||||
|
long mediaDurationUntilNextCheckpointUs =
|
||||||
|
Util.getMediaDurationForPlayoutDuration(
|
||||||
|
playoutDurationUntilNextCheckpointUs,
|
||||||
|
mediaPositionParameters.playbackParameters.speed);
|
||||||
|
return nextMediaPositionParameters.mediaTimeUs - mediaDurationUntilNextCheckpointUs;
|
||||||
}
|
}
|
||||||
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long applySkipping(long positionUs) {
|
private long applySkipping(long positionUs) {
|
||||||
|
@ -97,6 +97,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
private long currentPositionUs;
|
private long currentPositionUs;
|
||||||
private boolean allowFirstBufferPositionDiscontinuity;
|
private boolean allowFirstBufferPositionDiscontinuity;
|
||||||
private boolean allowPositionDiscontinuity;
|
private boolean allowPositionDiscontinuity;
|
||||||
|
private boolean audioSinkNeedsReset;
|
||||||
|
|
||||||
private boolean experimentalKeepAudioTrackOnSeek;
|
private boolean experimentalKeepAudioTrackOnSeek;
|
||||||
|
|
||||||
@ -507,6 +508,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDisabled() {
|
protected void onDisabled() {
|
||||||
|
audioSinkNeedsReset = true;
|
||||||
try {
|
try {
|
||||||
audioSink.flush();
|
audioSink.flush();
|
||||||
} finally {
|
} finally {
|
||||||
@ -523,7 +525,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
try {
|
try {
|
||||||
super.onReset();
|
super.onReset();
|
||||||
} finally {
|
} finally {
|
||||||
audioSink.reset();
|
if (audioSinkNeedsReset) {
|
||||||
|
audioSinkNeedsReset = false;
|
||||||
|
audioSink.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +83,14 @@ import java.util.Arrays;
|
|||||||
pitchBuffer = new short[maxRequiredFrameCount * channelCount];
|
pitchBuffer = new short[maxRequiredFrameCount * channelCount];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of bytes that have been input, but will not be processed until more input
|
||||||
|
* data is provided.
|
||||||
|
*/
|
||||||
|
public int getPendingInputBytes() {
|
||||||
|
return inputFrameCount * channelCount * BYTES_PER_SAMPLE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues remaining data from {@code buffer}, and advances its position by the number of bytes
|
* Queues remaining data from {@code buffer}, and advances its position by the number of bytes
|
||||||
* consumed.
|
* consumed.
|
||||||
|
@ -15,10 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.audio;
|
package com.google.android.exoplayer2.audio;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
@ -36,10 +37,10 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
|||||||
private static final float CLOSE_THRESHOLD = 0.01f;
|
private static final float CLOSE_THRESHOLD = 0.01f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum number of output bytes at which the speedup is calculated using the input/output
|
* The minimum number of output bytes required for duration scaling to be calculated using the
|
||||||
* byte counts, rather than using the current playback parameters speed.
|
* input and output byte counts, rather than using the current playback speed.
|
||||||
*/
|
*/
|
||||||
private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024;
|
private static final int MIN_BYTES_FOR_DURATION_SCALING_CALCULATION = 1024;
|
||||||
|
|
||||||
private int pendingOutputSampleRate;
|
private int pendingOutputSampleRate;
|
||||||
private float speed;
|
private float speed;
|
||||||
@ -74,35 +75,31 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the playback speed. This method may only be called after draining data through the
|
* Sets the target playback speed. This method may only be called after draining data through the
|
||||||
* processor. The value returned by {@link #isActive()} may change, and the processor must be
|
* processor. The value returned by {@link #isActive()} may change, and the processor must be
|
||||||
* {@link #flush() flushed} before queueing more data.
|
* {@link #flush() flushed} before queueing more data.
|
||||||
*
|
*
|
||||||
* @param speed The requested new playback speed.
|
* @param speed The target playback speed.
|
||||||
* @return The actual new playback speed.
|
|
||||||
*/
|
*/
|
||||||
public float setSpeed(float speed) {
|
public void setSpeed(float speed) {
|
||||||
if (this.speed != speed) {
|
if (this.speed != speed) {
|
||||||
this.speed = speed;
|
this.speed = speed;
|
||||||
pendingSonicRecreation = true;
|
pendingSonicRecreation = true;
|
||||||
}
|
}
|
||||||
return speed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the playback pitch. This method may only be called after draining data through the
|
* Sets the target playback pitch. This method may only be called after draining data through the
|
||||||
* processor. The value returned by {@link #isActive()} may change, and the processor must be
|
* processor. The value returned by {@link #isActive()} may change, and the processor must be
|
||||||
* {@link #flush() flushed} before queueing more data.
|
* {@link #flush() flushed} before queueing more data.
|
||||||
*
|
*
|
||||||
* @param pitch The requested new pitch.
|
* @param pitch The target pitch.
|
||||||
* @return The actual new pitch.
|
|
||||||
*/
|
*/
|
||||||
public float setPitch(float pitch) {
|
public void setPitch(float pitch) {
|
||||||
if (this.pitch != pitch) {
|
if (this.pitch != pitch) {
|
||||||
this.pitch = pitch;
|
this.pitch = pitch;
|
||||||
pendingSonicRecreation = true;
|
pendingSonicRecreation = true;
|
||||||
}
|
}
|
||||||
return pitch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,23 +115,27 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the specified duration scaled to take into account the speedup factor of this instance,
|
* Returns the media duration corresponding to the specified playout duration, taking speed
|
||||||
* in the same units as {@code duration}.
|
* adjustment into account.
|
||||||
*
|
*
|
||||||
* @param duration The duration to scale taking into account speedup.
|
* <p>The scaling performed by this method will use the actual playback speed achieved by the
|
||||||
* @return The specified duration scaled to take into account speedup, in the same units as
|
* audio processor, on average, since it was last flushed. This may differ very slightly from the
|
||||||
* {@code duration}.
|
* target playback speed.
|
||||||
|
*
|
||||||
|
* @param playoutDuration The playout duration to scale.
|
||||||
|
* @return The corresponding media duration, in the same units as {@code duration}.
|
||||||
*/
|
*/
|
||||||
public long scaleDurationForSpeedup(long duration) {
|
public long getMediaDuration(long playoutDuration) {
|
||||||
if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) {
|
if (outputBytes >= MIN_BYTES_FOR_DURATION_SCALING_CALCULATION) {
|
||||||
|
long processedInputBytes = inputBytes - checkNotNull(sonic).getPendingInputBytes();
|
||||||
return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate
|
return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate
|
||||||
? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes)
|
? Util.scaleLargeTimestamp(playoutDuration, processedInputBytes, outputBytes)
|
||||||
: Util.scaleLargeTimestamp(
|
: Util.scaleLargeTimestamp(
|
||||||
duration,
|
playoutDuration,
|
||||||
inputBytes * outputAudioFormat.sampleRate,
|
processedInputBytes * outputAudioFormat.sampleRate,
|
||||||
outputBytes * inputAudioFormat.sampleRate);
|
outputBytes * inputAudioFormat.sampleRate);
|
||||||
} else {
|
} else {
|
||||||
return (long) ((double) speed * duration);
|
return (long) ((double) speed * playoutDuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +165,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void queueInput(ByteBuffer inputBuffer) {
|
public void queueInput(ByteBuffer inputBuffer) {
|
||||||
Sonic sonic = Assertions.checkNotNull(this.sonic);
|
Sonic sonic = checkNotNull(this.sonic);
|
||||||
if (inputBuffer.hasRemaining()) {
|
if (inputBuffer.hasRemaining()) {
|
||||||
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
|
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
|
||||||
int inputSize = inputBuffer.remaining();
|
int inputSize = inputBuffer.remaining();
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.drm;
|
package com.google.android.exoplayer2.drm;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
@ -27,6 +28,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCode
|
|||||||
import com.google.android.exoplayer2.upstream.StatsDataSource;
|
import com.google.android.exoplayer2.upstream.StatsDataSource;
|
||||||
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 com.google.common.collect.ImmutableMap;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -39,29 +41,35 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
|||||||
private static final int MAX_MANUAL_REDIRECTS = 5;
|
private static final int MAX_MANUAL_REDIRECTS = 5;
|
||||||
|
|
||||||
private final HttpDataSource.Factory dataSourceFactory;
|
private final HttpDataSource.Factory dataSourceFactory;
|
||||||
private final String defaultLicenseUrl;
|
@Nullable private final String defaultLicenseUrl;
|
||||||
private final boolean forceDefaultLicenseUrl;
|
private final boolean forceDefaultLicenseUrl;
|
||||||
private final Map<String, String> keyRequestProperties;
|
private final Map<String, String> keyRequestProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
|
* @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
|
||||||
* their own license URL.
|
* their own license URL. May be {@code null} if it's known that all key requests will specify
|
||||||
|
* their own URLs.
|
||||||
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
|
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
|
||||||
*/
|
*/
|
||||||
public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
|
public HttpMediaDrmCallback(
|
||||||
|
@Nullable String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
|
||||||
this(defaultLicenseUrl, /* forceDefaultLicenseUrl= */ false, dataSourceFactory);
|
this(defaultLicenseUrl, /* forceDefaultLicenseUrl= */ false, dataSourceFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
|
* @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
|
||||||
* their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is
|
* their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is set to
|
||||||
* set to true.
|
* true. May be {@code null} if {@code forceDefaultLicenseUrl} is {@code false} and if it's
|
||||||
* @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
|
* known that all key requests will specify their own URLs.
|
||||||
* include their own license URL.
|
* @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} for key
|
||||||
|
* requests that include their own license URL.
|
||||||
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
|
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
|
||||||
*/
|
*/
|
||||||
public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,
|
public HttpMediaDrmCallback(
|
||||||
|
@Nullable String defaultLicenseUrl,
|
||||||
|
boolean forceDefaultLicenseUrl,
|
||||||
HttpDataSource.Factory dataSourceFactory) {
|
HttpDataSource.Factory dataSourceFactory) {
|
||||||
|
Assertions.checkArgument(!(forceDefaultLicenseUrl && TextUtils.isEmpty(defaultLicenseUrl)));
|
||||||
this.dataSourceFactory = dataSourceFactory;
|
this.dataSourceFactory = dataSourceFactory;
|
||||||
this.defaultLicenseUrl = defaultLicenseUrl;
|
this.defaultLicenseUrl = defaultLicenseUrl;
|
||||||
this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
|
this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
|
||||||
@ -121,6 +129,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
|||||||
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
|
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
|
||||||
url = defaultLicenseUrl;
|
url = defaultLicenseUrl;
|
||||||
}
|
}
|
||||||
|
if (TextUtils.isEmpty(url)) {
|
||||||
|
throw new MediaDrmCallbackException(
|
||||||
|
new DataSpec.Builder().setUri(Uri.EMPTY).build(),
|
||||||
|
Uri.EMPTY,
|
||||||
|
/* responseHeaders= */ ImmutableMap.of(),
|
||||||
|
/* bytesLoaded= */ 0,
|
||||||
|
/* cause= */ new IllegalStateException("No license URL"));
|
||||||
|
}
|
||||||
Map<String, String> requestProperties = new HashMap<>();
|
Map<String, String> requestProperties = new HashMap<>();
|
||||||
// Add standard request properties for supported schemes.
|
// Add standard request properties for supported schemes.
|
||||||
String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml"
|
String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml"
|
||||||
|
@ -38,6 +38,7 @@ public final class ProgressiveDownloader implements Downloader {
|
|||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final DataSpec dataSpec;
|
private final DataSpec dataSpec;
|
||||||
private final CacheDataSource dataSource;
|
private final CacheDataSource dataSource;
|
||||||
|
private final CacheWriter cacheWriter;
|
||||||
@Nullable private final PriorityTaskManager priorityTaskManager;
|
@Nullable private final PriorityTaskManager priorityTaskManager;
|
||||||
|
|
||||||
@Nullable private ProgressListener progressListener;
|
@Nullable private ProgressListener progressListener;
|
||||||
@ -101,6 +102,15 @@ public final class ProgressiveDownloader implements Downloader {
|
|||||||
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
|
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
|
||||||
.build();
|
.build();
|
||||||
dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
|
dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
|
||||||
|
@SuppressWarnings("methodref.receiver.bound.invalid")
|
||||||
|
CacheWriter.ProgressListener progressListener = this::onProgress;
|
||||||
|
cacheWriter =
|
||||||
|
new CacheWriter(
|
||||||
|
dataSource,
|
||||||
|
dataSpec,
|
||||||
|
/* allowShortContent= */ false,
|
||||||
|
/* temporaryBuffer= */ null,
|
||||||
|
progressListener);
|
||||||
priorityTaskManager = cacheDataSourceFactory.getUpstreamPriorityTaskManager();
|
priorityTaskManager = cacheDataSourceFactory.getUpstreamPriorityTaskManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,28 +118,19 @@ public final class ProgressiveDownloader implements Downloader {
|
|||||||
public void download(@Nullable ProgressListener progressListener)
|
public void download(@Nullable ProgressListener progressListener)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
this.progressListener = progressListener;
|
this.progressListener = progressListener;
|
||||||
if (downloadRunnable == null) {
|
downloadRunnable =
|
||||||
CacheWriter cacheWriter =
|
new RunnableFutureTask<Void, IOException>() {
|
||||||
new CacheWriter(
|
@Override
|
||||||
dataSource,
|
protected Void doWork() throws IOException {
|
||||||
dataSpec,
|
cacheWriter.cache();
|
||||||
/* allowShortContent= */ false,
|
return null;
|
||||||
/* temporaryBuffer= */ null,
|
}
|
||||||
this::onProgress);
|
|
||||||
downloadRunnable =
|
|
||||||
new RunnableFutureTask<Void, IOException>() {
|
|
||||||
@Override
|
|
||||||
protected Void doWork() throws IOException {
|
|
||||||
cacheWriter.cache();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void cancelWork() {
|
protected void cancelWork() {
|
||||||
cacheWriter.cancel();
|
cacheWriter.cancel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (priorityTaskManager != null) {
|
if (priorityTaskManager != null) {
|
||||||
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
|
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
|
||||||
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source;
|
|||||||
|
|
||||||
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
|
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
|
||||||
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
|
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
|
||||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
@ -68,7 +67,7 @@ public final class MediaSourceDrmHelper {
|
|||||||
Assertions.checkNotNull(mediaItem.playbackProperties);
|
Assertions.checkNotNull(mediaItem.playbackProperties);
|
||||||
@Nullable
|
@Nullable
|
||||||
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
|
||||||
if (drmConfiguration == null || drmConfiguration.licenseUri == null || Util.SDK_INT < 18) {
|
if (drmConfiguration == null || Util.SDK_INT < 18) {
|
||||||
return DrmSessionManager.getDummyDrmSessionManager();
|
return DrmSessionManager.getDummyDrmSessionManager();
|
||||||
}
|
}
|
||||||
HttpDataSource.Factory dataSourceFactory =
|
HttpDataSource.Factory dataSourceFactory =
|
||||||
@ -77,7 +76,7 @@ public final class MediaSourceDrmHelper {
|
|||||||
: new DefaultHttpDataSourceFactory(userAgent != null ? userAgent : DEFAULT_USER_AGENT);
|
: new DefaultHttpDataSourceFactory(userAgent != null ? userAgent : DEFAULT_USER_AGENT);
|
||||||
HttpMediaDrmCallback httpDrmCallback =
|
HttpMediaDrmCallback httpDrmCallback =
|
||||||
new HttpMediaDrmCallback(
|
new HttpMediaDrmCallback(
|
||||||
castNonNull(drmConfiguration.licenseUri).toString(),
|
drmConfiguration.licenseUri == null ? null : drmConfiguration.licenseUri.toString(),
|
||||||
drmConfiguration.forceDefaultLicenseUri,
|
drmConfiguration.forceDefaultLicenseUri,
|
||||||
dataSourceFactory);
|
dataSourceFactory);
|
||||||
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
|
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
|
||||||
|
@ -435,6 +435,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
pendingResetPositionUs = positionUs;
|
pendingResetPositionUs = positionUs;
|
||||||
loadingFinished = false;
|
loadingFinished = false;
|
||||||
if (loader.isLoading()) {
|
if (loader.isLoading()) {
|
||||||
|
// Discard as much as we can synchronously.
|
||||||
|
for (SampleQueue sampleQueue : sampleQueues) {
|
||||||
|
sampleQueue.discardToEnd();
|
||||||
|
}
|
||||||
loader.cancelLoading();
|
loader.cancelLoading();
|
||||||
} else {
|
} else {
|
||||||
loader.clearFatalError();
|
loader.clearFatalError();
|
||||||
|
@ -894,6 +894,11 @@ public class SampleQueue implements TrackOutput {
|
|||||||
if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
|
if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
|
||||||
// We've found a suitable sample.
|
// We've found a suitable sample.
|
||||||
sampleCountToTarget = i;
|
sampleCountToTarget = i;
|
||||||
|
if (timesUs[searchIndex] == timeUs) {
|
||||||
|
// Stop the search if we found a sample at the specified time to avoid returning a later
|
||||||
|
// sample with the same exactly matching timestamp.
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
searchIndex++;
|
searchIndex++;
|
||||||
if (searchIndex == capacity) {
|
if (searchIndex == capacity) {
|
||||||
|
@ -315,6 +315,11 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
|||||||
mediaChunks.clear();
|
mediaChunks.clear();
|
||||||
nextNotifyPrimaryFormatMediaChunkIndex = 0;
|
nextNotifyPrimaryFormatMediaChunkIndex = 0;
|
||||||
if (loader.isLoading()) {
|
if (loader.isLoading()) {
|
||||||
|
// Discard as much as we can synchronously.
|
||||||
|
primarySampleQueue.discardToEnd();
|
||||||
|
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
|
||||||
|
embeddedSampleQueue.discardToEnd();
|
||||||
|
}
|
||||||
loader.cancelLoading();
|
loader.cancelLoading();
|
||||||
} else {
|
} else {
|
||||||
loader.clearFatalError();
|
loader.clearFatalError();
|
||||||
|
@ -31,6 +31,7 @@ import com.google.android.exoplayer2.text.Cue;
|
|||||||
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
|
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
|
||||||
import com.google.android.exoplayer2.text.Subtitle;
|
import com.google.android.exoplayer2.text.Subtitle;
|
||||||
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
@ -43,6 +44,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
||||||
|
|
||||||
|
private static final String TAG = "Tx3gDecoder";
|
||||||
|
|
||||||
private static final char BOM_UTF16_BE = '\uFEFF';
|
private static final char BOM_UTF16_BE = '\uFEFF';
|
||||||
private static final char BOM_UTF16_LE = '\uFFFE';
|
private static final char BOM_UTF16_LE = '\uFFFE';
|
||||||
|
|
||||||
@ -185,6 +188,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
|
|||||||
int fontFace = parsableByteArray.readUnsignedByte();
|
int fontFace = parsableByteArray.readUnsignedByte();
|
||||||
parsableByteArray.skipBytes(1); // font size
|
parsableByteArray.skipBytes(1); // font size
|
||||||
int colorRgba = parsableByteArray.readInt();
|
int colorRgba = parsableByteArray.readInt();
|
||||||
|
|
||||||
|
if (end > cueText.length()) {
|
||||||
|
Log.w(
|
||||||
|
TAG, "Truncating styl end (" + end + ") to cueText.length() (" + cueText.length() + ").");
|
||||||
|
end = cueText.length();
|
||||||
|
}
|
||||||
|
if (start >= end) {
|
||||||
|
Log.w(TAG, "Ignoring styl with start (" + start + ") >= end (" + end + ").");
|
||||||
|
return;
|
||||||
|
}
|
||||||
attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH);
|
attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH);
|
||||||
attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);
|
attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);
|
||||||
}
|
}
|
||||||
|
@ -306,6 +306,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
|||||||
try {
|
try {
|
||||||
connection = makeConnection(dataSpec);
|
connection = makeConnection(dataSpec);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@Nullable String message = e.getMessage();
|
||||||
|
if (message != null
|
||||||
|
&& Util.toLowerInvariant(message).matches("cleartext http traffic.*not permitted.*")) {
|
||||||
|
throw new CleartextNotPermittedException(e, dataSpec);
|
||||||
|
}
|
||||||
throw new HttpDataSourceException(
|
throw new HttpDataSourceException(
|
||||||
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import static java.lang.Math.min;
|
|||||||
|
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.upstream.HttpDataSource.CleartextNotPermittedException;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
|
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
|
||||||
import com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException;
|
import com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@ -86,14 +87,16 @@ public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retries for any exception that is not a subclass of {@link ParserException}, {@link
|
* Retries for any exception that is not a subclass of {@link ParserException}, {@link
|
||||||
* FileNotFoundException} or {@link UnexpectedLoaderException}. The retry delay is calculated as
|
* FileNotFoundException}, {@link CleartextNotPermittedException} or {@link
|
||||||
* {@code Math.min((errorCount - 1) * 1000, 5000)}.
|
* UnexpectedLoaderException}. The retry delay is calculated as {@code Math.min((errorCount - 1) *
|
||||||
|
* 1000, 5000)}.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
|
public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
|
||||||
IOException exception = loadErrorInfo.exception;
|
IOException exception = loadErrorInfo.exception;
|
||||||
return exception instanceof ParserException
|
return exception instanceof ParserException
|
||||||
|| exception instanceof FileNotFoundException
|
|| exception instanceof FileNotFoundException
|
||||||
|
|| exception instanceof CleartextNotPermittedException
|
||||||
|| exception instanceof UnexpectedLoaderException
|
|| exception instanceof UnexpectedLoaderException
|
||||||
? C.TIME_UNSET
|
? C.TIME_UNSET
|
||||||
: min((loadErrorInfo.errorCount - 1) * 1000, 5000);
|
: min((loadErrorInfo.errorCount - 1) * 1000, 5000);
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.offline;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||||
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.Cache;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit tests for {@link ActionFile}. */
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ProgressiveDownloaderTest {
|
||||||
|
|
||||||
|
private File testDir;
|
||||||
|
private Cache downloadCache;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void createDownloadCache() throws Exception {
|
||||||
|
testDir =
|
||||||
|
Util.createTempFile(
|
||||||
|
ApplicationProvider.getApplicationContext(), "ProgressiveDownloaderTest");
|
||||||
|
assertThat(testDir.delete()).isTrue();
|
||||||
|
assertThat(testDir.mkdirs()).isTrue();
|
||||||
|
|
||||||
|
DatabaseProvider databaseProvider = TestUtil.getInMemoryDatabaseProvider();
|
||||||
|
downloadCache = new SimpleCache(testDir, new NoOpCacheEvictor(), databaseProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void deleteDownloadCache() {
|
||||||
|
downloadCache.release();
|
||||||
|
Util.recursiveDelete(testDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void download_afterSingleFailure_succeeds() throws Exception {
|
||||||
|
Uri uri = Uri.parse("test:///test.mp4");
|
||||||
|
|
||||||
|
// Fake data has a built in failure after 10 bytes.
|
||||||
|
FakeDataSet data = new FakeDataSet();
|
||||||
|
data.newData(uri).appendReadData(10).appendReadError(new IOException()).appendReadData(20);
|
||||||
|
DataSource.Factory upstreamDataSource = new FakeDataSource.Factory().setFakeDataSet(data);
|
||||||
|
|
||||||
|
MediaItem mediaItem = MediaItem.fromUri(uri);
|
||||||
|
CacheDataSource.Factory cacheDataSourceFactory =
|
||||||
|
new CacheDataSource.Factory()
|
||||||
|
.setCache(downloadCache)
|
||||||
|
.setUpstreamDataSourceFactory(upstreamDataSource);
|
||||||
|
ProgressiveDownloader downloader = new ProgressiveDownloader(mediaItem, cacheDataSourceFactory);
|
||||||
|
|
||||||
|
TestProgressListener progressListener = new TestProgressListener();
|
||||||
|
|
||||||
|
// Failure expected after 10 bytes.
|
||||||
|
assertThrows(IOException.class, () -> downloader.download(progressListener));
|
||||||
|
assertThat(progressListener.bytesDownloaded).isEqualTo(10);
|
||||||
|
|
||||||
|
// Retry should succeed.
|
||||||
|
downloader.download(progressListener);
|
||||||
|
assertThat(progressListener.bytesDownloaded).isEqualTo(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestProgressListener implements Downloader.ProgressListener {
|
||||||
|
|
||||||
|
public long bytesDownloaded;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {
|
||||||
|
this.bytesDownloaded = bytesDownloaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -861,6 +861,53 @@ public final class SampleQueueTest {
|
|||||||
assertAllocationCount(1);
|
assertAllocationCount(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void discardTo_withDuplicateTimestamps_discardsOnlyToFirstMatch() {
|
||||||
|
writeTestData(
|
||||||
|
DATA,
|
||||||
|
SAMPLE_SIZES,
|
||||||
|
SAMPLE_OFFSETS,
|
||||||
|
/* sampleTimestamps= */ new long[] {0, 1000, 1000, 1000, 2000, 2000, 2000, 2000},
|
||||||
|
SAMPLE_FORMATS,
|
||||||
|
/* sampleFlags= */ new int[] {
|
||||||
|
BUFFER_FLAG_KEY_FRAME,
|
||||||
|
0,
|
||||||
|
BUFFER_FLAG_KEY_FRAME,
|
||||||
|
BUFFER_FLAG_KEY_FRAME,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
BUFFER_FLAG_KEY_FRAME,
|
||||||
|
BUFFER_FLAG_KEY_FRAME
|
||||||
|
});
|
||||||
|
|
||||||
|
// Discard to first keyframe exactly matching the specified time.
|
||||||
|
sampleQueue.discardTo(
|
||||||
|
/* timeUs= */ 1000, /* toKeyframe= */ true, /* stopAtReadPosition= */ false);
|
||||||
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(2);
|
||||||
|
|
||||||
|
// Do nothing when trying again.
|
||||||
|
sampleQueue.discardTo(
|
||||||
|
/* timeUs= */ 1000, /* toKeyframe= */ true, /* stopAtReadPosition= */ false);
|
||||||
|
sampleQueue.discardTo(
|
||||||
|
/* timeUs= */ 1000, /* toKeyframe= */ false, /* stopAtReadPosition= */ false);
|
||||||
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(2);
|
||||||
|
|
||||||
|
// Discard to first frame exactly matching the specified time.
|
||||||
|
sampleQueue.discardTo(
|
||||||
|
/* timeUs= */ 2000, /* toKeyframe= */ false, /* stopAtReadPosition= */ false);
|
||||||
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(4);
|
||||||
|
|
||||||
|
// Do nothing when trying again.
|
||||||
|
sampleQueue.discardTo(
|
||||||
|
/* timeUs= */ 2000, /* toKeyframe= */ false, /* stopAtReadPosition= */ false);
|
||||||
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(4);
|
||||||
|
|
||||||
|
// Discard to first keyframe at same timestamp.
|
||||||
|
sampleQueue.discardTo(
|
||||||
|
/* timeUs= */ 2000, /* toKeyframe= */ true, /* stopAtReadPosition= */ false);
|
||||||
|
assertThat(sampleQueue.getFirstIndex()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void discardToDontStopAtReadPosition() {
|
public void discardToDontStopAtReadPosition() {
|
||||||
writeTestData();
|
writeTestData();
|
||||||
|
@ -15,24 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.text.tx3g;
|
package com.google.android.exoplayer2.text.tx3g;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.text.SpannedString;
|
import android.text.SpannedString;
|
||||||
import android.text.style.ForegroundColorSpan;
|
|
||||||
import android.text.style.StyleSpan;
|
|
||||||
import android.text.style.TypefaceSpan;
|
|
||||||
import android.text.style.UnderlineSpan;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.text.Subtitle;
|
import com.google.android.exoplayer2.text.Subtitle;
|
||||||
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@ -44,6 +38,10 @@ public final class Tx3gDecoderTest {
|
|||||||
private static final String NO_SUBTITLE = "media/tx3g/no_subtitle";
|
private static final String NO_SUBTITLE = "media/tx3g/no_subtitle";
|
||||||
private static final String SAMPLE_JUST_TEXT = "media/tx3g/sample_just_text";
|
private static final String SAMPLE_JUST_TEXT = "media/tx3g/sample_just_text";
|
||||||
private static final String SAMPLE_WITH_STYL = "media/tx3g/sample_with_styl";
|
private static final String SAMPLE_WITH_STYL = "media/tx3g/sample_with_styl";
|
||||||
|
private static final String SAMPLE_WITH_STYL_START_TOO_LARGE =
|
||||||
|
"media/tx3g/sample_with_styl_start_too_large";
|
||||||
|
private static final String SAMPLE_WITH_STYL_END_TOO_LARGE =
|
||||||
|
"media/tx3g/sample_with_styl_end_too_large";
|
||||||
private static final String SAMPLE_WITH_STYL_ALL_DEFAULTS =
|
private static final String SAMPLE_WITH_STYL_ALL_DEFAULTS =
|
||||||
"media/tx3g/sample_with_styl_all_defaults";
|
"media/tx3g/sample_with_styl_all_defaults";
|
||||||
private static final String SAMPLE_UTF16_BE_NO_STYL = "media/tx3g/sample_utf16_be_no_styl";
|
private static final String SAMPLE_UTF16_BE_NO_STYL = "media/tx3g/sample_utf16_be_no_styl";
|
||||||
@ -57,197 +55,230 @@ public final class Tx3gDecoderTest {
|
|||||||
"media/tx3g/initialization_all_defaults";
|
"media/tx3g/initialization_all_defaults";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeNoSubtitle() throws IOException, SubtitleDecoderException {
|
public void decodeNoSubtitle() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_SUBTITLE);
|
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_SUBTITLE);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
assertThat(subtitle.getCues(0)).isEmpty();
|
assertThat(subtitle.getCues(0)).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeJustText() throws IOException, SubtitleDecoderException {
|
public void decodeJustText() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_JUST_TEXT);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_JUST_TEXT);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("CC Test");
|
assertThat(text.toString()).isEqualTo("CC Test");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);
|
assertThat(text).hasNoSpans();
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeWithStyl() throws IOException, SubtitleDecoderException {
|
public void decodeWithStyl() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("CC Test");
|
assertThat(text.toString()).isEqualTo("CC Test");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(3);
|
assertThat(text).hasBoldItalicSpanBetween(0, 6);
|
||||||
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
|
assertThat(text).hasUnderlineSpanBetween(0, 6);
|
||||||
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
|
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
|
||||||
findSpan(text, 0, 6, UnderlineSpan.class);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
}
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
|
|
||||||
|
/**
|
||||||
|
* The 7-byte sample contains a 4-byte emoji. The start index (6) and end index (7) are valid as
|
||||||
|
* byte offsets, but not a UTF-16 code-unit offset, so they're both truncated to 5 (the length of
|
||||||
|
* the resulting the string in Java) and the spans end up empty (so we don't add them).
|
||||||
|
*
|
||||||
|
* <p>https://github.com/google/ExoPlayer/pull/8133
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void decodeWithStyl_startTooLarge_noSpanAdded() throws Exception {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
|
byte[] bytes =
|
||||||
|
TestUtil.getByteArray(
|
||||||
|
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_START_TOO_LARGE);
|
||||||
|
|
||||||
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
|
|
||||||
|
assertThat(text.toString()).isEqualTo("CC 🙂");
|
||||||
|
assertThat(text).hasNoSpans();
|
||||||
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 7-byte sample contains a 4-byte emoji. The end index (6) is valid as a byte offset, but not
|
||||||
|
* a UTF-16 code-unit offset, so it's truncated to 5 (the length of the resulting the string in
|
||||||
|
* Java).
|
||||||
|
*
|
||||||
|
* <p>https://github.com/google/ExoPlayer/pull/8133
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void decodeWithStyl_endTooLarge_clippedToEndOfText() throws Exception {
|
||||||
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
|
byte[] bytes =
|
||||||
|
TestUtil.getByteArray(
|
||||||
|
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_END_TOO_LARGE);
|
||||||
|
|
||||||
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
|
|
||||||
|
assertThat(text.toString()).isEqualTo("CC 🙂");
|
||||||
|
assertThat(text).hasBoldItalicSpanBetween(0, 5);
|
||||||
|
assertThat(text).hasUnderlineSpanBetween(0, 5);
|
||||||
|
assertThat(text).hasForegroundColorSpanBetween(0, 5).withColor(Color.GREEN);
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeWithStylAllDefaults() throws IOException, SubtitleDecoderException {
|
public void decodeWithStylAllDefaults() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(
|
TestUtil.getByteArray(
|
||||||
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_ALL_DEFAULTS);
|
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_ALL_DEFAULTS);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("CC Test");
|
assertThat(text.toString()).isEqualTo("CC Test");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);
|
assertThat(text).hasNoSpans();
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException {
|
public void decodeUtf16BeNoStyl() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_BE_NO_STYL);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_BE_NO_STYL);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("你好");
|
assertThat(text.toString()).isEqualTo("你好");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);
|
assertThat(text).hasNoSpans();
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException {
|
public void decodeUtf16LeNoStyl() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_LE_NO_STYL);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_LE_NO_STYL);
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
|
|
||||||
assertThat(text.toString()).isEqualTo("你好");
|
assertThat(text.toString()).isEqualTo("你好");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(0);
|
assertThat(text).hasNoSpans();
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeWithMultipleStyl() throws IOException, SubtitleDecoderException {
|
public void decodeWithMultipleStyl() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(
|
TestUtil.getByteArray(
|
||||||
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_MULTIPLE_STYL);
|
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_MULTIPLE_STYL);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("Line 2\nLine 3");
|
assertThat(text.toString()).isEqualTo("Line 2\nLine 3");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(4);
|
assertThat(text).hasItalicSpanBetween(0, 5);
|
||||||
StyleSpan styleSpan = findSpan(text, 0, 5, StyleSpan.class);
|
assertThat(text).hasUnderlineSpanBetween(7, 12);
|
||||||
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.ITALIC);
|
assertThat(text).hasForegroundColorSpanBetween(0, 5).withColor(Color.GREEN);
|
||||||
findSpan(text, 7, 12, UnderlineSpan.class);
|
assertThat(text).hasForegroundColorSpanBetween(7, 12).withColor(Color.GREEN);
|
||||||
ForegroundColorSpan colorSpan = findSpan(text, 0, 5, ForegroundColorSpan.class);
|
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
|
|
||||||
colorSpan = findSpan(text, 7, 12, ForegroundColorSpan.class);
|
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
|
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeWithOtherExtension() throws IOException, SubtitleDecoderException {
|
public void decodeWithOtherExtension() throws Exception {
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
|
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(
|
TestUtil.getByteArray(
|
||||||
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_OTHER_EXTENSION);
|
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_OTHER_EXTENSION);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("CC Test");
|
assertThat(text.toString()).isEqualTo("CC Test");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(2);
|
assertThat(text).hasBoldSpanBetween(0, 6);
|
||||||
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
|
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
|
||||||
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD);
|
|
||||||
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
|
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void initializationDecodeWithStyl() throws IOException, SubtitleDecoderException {
|
public void initializationDecodeWithStyl() throws Exception {
|
||||||
byte[] initBytes =
|
byte[] initBytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("CC Test");
|
assertThat(text.toString()).isEqualTo("CC Test");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(5);
|
assertThat(text).hasBoldItalicSpanBetween(0, 7);
|
||||||
StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);
|
assertThat(text).hasUnderlineSpanBetween(0, 7);
|
||||||
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
|
assertThat(text).hasTypefaceSpanBetween(0, 7).withFamily(C.SERIF_NAME);
|
||||||
findSpan(text, 0, text.length(), UnderlineSpan.class);
|
// TODO(internal b/171984212): Fix Tx3gDecoder to avoid overlapping spans of the same type.
|
||||||
TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class);
|
assertThat(text).hasForegroundColorSpanBetween(0, 7).withColor(Color.RED);
|
||||||
assertThat(typefaceSpan.getFamily()).isEqualTo(C.SERIF_NAME);
|
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
|
||||||
ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class);
|
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.RED);
|
|
||||||
colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
|
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void initializationDecodeWithTbox() throws IOException, SubtitleDecoderException {
|
public void initializationDecodeWithTbox() throws Exception {
|
||||||
byte[] initBytes =
|
byte[] initBytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_TBOX);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_TBOX);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("CC Test");
|
assertThat(text.toString()).isEqualTo("CC Test");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(4);
|
assertThat(text).hasBoldItalicSpanBetween(0, 7);
|
||||||
StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);
|
assertThat(text).hasUnderlineSpanBetween(0, 7);
|
||||||
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
|
assertThat(text).hasTypefaceSpanBetween(0, 7).withFamily(C.SERIF_NAME);
|
||||||
findSpan(text, 0, text.length(), UnderlineSpan.class);
|
assertThat(text).hasForegroundColorSpanBetween(0, 7).withColor(Color.RED);
|
||||||
TypefaceSpan typefaceSpan = findSpan(text, 0, text.length(), TypefaceSpan.class);
|
|
||||||
assertThat(typefaceSpan.getFamily()).isEqualTo(C.SERIF_NAME);
|
|
||||||
ForegroundColorSpan colorSpan = findSpan(text, 0, text.length(), ForegroundColorSpan.class);
|
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.RED);
|
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1875f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1875f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void initializationAllDefaultsDecodeWithStyl()
|
public void initializationAllDefaultsDecodeWithStyl() throws Exception {
|
||||||
throws IOException, SubtitleDecoderException {
|
|
||||||
byte[] initBytes =
|
byte[] initBytes =
|
||||||
TestUtil.getByteArray(
|
TestUtil.getByteArray(
|
||||||
ApplicationProvider.getApplicationContext(), INITIALIZATION_ALL_DEFAULTS);
|
ApplicationProvider.getApplicationContext(), INITIALIZATION_ALL_DEFAULTS);
|
||||||
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
|
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
|
||||||
|
|
||||||
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
|
||||||
|
|
||||||
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
|
||||||
assertThat(text.toString()).isEqualTo("CC Test");
|
assertThat(text.toString()).isEqualTo("CC Test");
|
||||||
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(3);
|
assertThat(text).hasBoldItalicSpanBetween(0, 6);
|
||||||
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
|
assertThat(text).hasUnderlineSpanBetween(0, 6);
|
||||||
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
|
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
|
||||||
findSpan(text, 0, 6, UnderlineSpan.class);
|
|
||||||
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
|
|
||||||
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
|
|
||||||
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> T findSpan(
|
|
||||||
SpannedString testObject, int expectedStart, int expectedEnd, Class<T> expectedType) {
|
|
||||||
T[] spans = testObject.getSpans(0, testObject.length(), expectedType);
|
|
||||||
for (T span : spans) {
|
|
||||||
if (testObject.getSpanStart(span) == expectedStart
|
|
||||||
&& testObject.getSpanEnd(span) == expectedEnd) {
|
|
||||||
return span;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fail("Span not found.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertFractionalLinePosition(Cue cue, float expectedFraction) {
|
private static void assertFractionalLinePosition(Cue cue, float expectedFraction) {
|
||||||
assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
|
assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
|
||||||
assertThat(cue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
|
assertThat(cue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
|
||||||
assertThat(Math.abs(expectedFraction - cue.line) < 1e-6).isTrue();
|
assertThat(cue.line).isWithin(1e-6f).of(expectedFraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,6 +128,8 @@ public class MatroskaExtractor implements Extractor {
|
|||||||
private static final String CODEC_ID_FLAC = "A_FLAC";
|
private static final String CODEC_ID_FLAC = "A_FLAC";
|
||||||
private static final String CODEC_ID_ACM = "A_MS/ACM";
|
private static final String CODEC_ID_ACM = "A_MS/ACM";
|
||||||
private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT";
|
private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT";
|
||||||
|
private static final String CODEC_ID_PCM_INT_BIG = "A_PCM/INT/BIG";
|
||||||
|
private static final String CODEC_ID_PCM_FLOAT = "A_PCM/FLOAT/IEEE";
|
||||||
private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
|
private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
|
||||||
private static final String CODEC_ID_ASS = "S_TEXT/ASS";
|
private static final String CODEC_ID_ASS = "S_TEXT/ASS";
|
||||||
private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
|
private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
|
||||||
@ -1743,36 +1745,43 @@ public class MatroskaExtractor implements Extractor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isCodecSupported(String codecId) {
|
private static boolean isCodecSupported(String codecId) {
|
||||||
return CODEC_ID_VP8.equals(codecId)
|
switch (codecId) {
|
||||||
|| CODEC_ID_VP9.equals(codecId)
|
case CODEC_ID_VP8:
|
||||||
|| CODEC_ID_AV1.equals(codecId)
|
case CODEC_ID_VP9:
|
||||||
|| CODEC_ID_MPEG2.equals(codecId)
|
case CODEC_ID_AV1:
|
||||||
|| CODEC_ID_MPEG4_SP.equals(codecId)
|
case CODEC_ID_MPEG2:
|
||||||
|| CODEC_ID_MPEG4_ASP.equals(codecId)
|
case CODEC_ID_MPEG4_SP:
|
||||||
|| CODEC_ID_MPEG4_AP.equals(codecId)
|
case CODEC_ID_MPEG4_ASP:
|
||||||
|| CODEC_ID_H264.equals(codecId)
|
case CODEC_ID_MPEG4_AP:
|
||||||
|| CODEC_ID_H265.equals(codecId)
|
case CODEC_ID_H264:
|
||||||
|| CODEC_ID_FOURCC.equals(codecId)
|
case CODEC_ID_H265:
|
||||||
|| CODEC_ID_THEORA.equals(codecId)
|
case CODEC_ID_FOURCC:
|
||||||
|| CODEC_ID_OPUS.equals(codecId)
|
case CODEC_ID_THEORA:
|
||||||
|| CODEC_ID_VORBIS.equals(codecId)
|
case CODEC_ID_OPUS:
|
||||||
|| CODEC_ID_AAC.equals(codecId)
|
case CODEC_ID_VORBIS:
|
||||||
|| CODEC_ID_MP2.equals(codecId)
|
case CODEC_ID_AAC:
|
||||||
|| CODEC_ID_MP3.equals(codecId)
|
case CODEC_ID_MP2:
|
||||||
|| CODEC_ID_AC3.equals(codecId)
|
case CODEC_ID_MP3:
|
||||||
|| CODEC_ID_E_AC3.equals(codecId)
|
case CODEC_ID_AC3:
|
||||||
|| CODEC_ID_TRUEHD.equals(codecId)
|
case CODEC_ID_E_AC3:
|
||||||
|| CODEC_ID_DTS.equals(codecId)
|
case CODEC_ID_TRUEHD:
|
||||||
|| CODEC_ID_DTS_EXPRESS.equals(codecId)
|
case CODEC_ID_DTS:
|
||||||
|| CODEC_ID_DTS_LOSSLESS.equals(codecId)
|
case CODEC_ID_DTS_EXPRESS:
|
||||||
|| CODEC_ID_FLAC.equals(codecId)
|
case CODEC_ID_DTS_LOSSLESS:
|
||||||
|| CODEC_ID_ACM.equals(codecId)
|
case CODEC_ID_FLAC:
|
||||||
|| CODEC_ID_PCM_INT_LIT.equals(codecId)
|
case CODEC_ID_ACM:
|
||||||
|| CODEC_ID_SUBRIP.equals(codecId)
|
case CODEC_ID_PCM_INT_LIT:
|
||||||
|| CODEC_ID_ASS.equals(codecId)
|
case CODEC_ID_PCM_INT_BIG:
|
||||||
|| CODEC_ID_VOBSUB.equals(codecId)
|
case CODEC_ID_PCM_FLOAT:
|
||||||
|| CODEC_ID_PGS.equals(codecId)
|
case CODEC_ID_SUBRIP:
|
||||||
|| CODEC_ID_DVBSUB.equals(codecId);
|
case CODEC_ID_ASS:
|
||||||
|
case CODEC_ID_VOBSUB:
|
||||||
|
case CODEC_ID_PGS:
|
||||||
|
case CODEC_ID_DVBSUB:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2102,8 +2111,44 @@ public class MatroskaExtractor implements Extractor {
|
|||||||
if (pcmEncoding == C.ENCODING_INVALID) {
|
if (pcmEncoding == C.ENCODING_INVALID) {
|
||||||
pcmEncoding = Format.NO_VALUE;
|
pcmEncoding = Format.NO_VALUE;
|
||||||
mimeType = MimeTypes.AUDIO_UNKNOWN;
|
mimeType = MimeTypes.AUDIO_UNKNOWN;
|
||||||
Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
|
Log.w(
|
||||||
+ mimeType);
|
TAG,
|
||||||
|
"Unsupported little endian PCM bit depth: "
|
||||||
|
+ audioBitDepth
|
||||||
|
+ ". Setting mimeType to "
|
||||||
|
+ mimeType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CODEC_ID_PCM_INT_BIG:
|
||||||
|
mimeType = MimeTypes.AUDIO_RAW;
|
||||||
|
if (audioBitDepth == 8) {
|
||||||
|
pcmEncoding = C.ENCODING_PCM_8BIT;
|
||||||
|
} else if (audioBitDepth == 16) {
|
||||||
|
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
|
||||||
|
} else {
|
||||||
|
pcmEncoding = Format.NO_VALUE;
|
||||||
|
mimeType = MimeTypes.AUDIO_UNKNOWN;
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Unsupported big endian PCM bit depth: "
|
||||||
|
+ audioBitDepth
|
||||||
|
+ ". Setting mimeType to "
|
||||||
|
+ mimeType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CODEC_ID_PCM_FLOAT:
|
||||||
|
mimeType = MimeTypes.AUDIO_RAW;
|
||||||
|
if (audioBitDepth == 32) {
|
||||||
|
pcmEncoding = C.ENCODING_PCM_FLOAT;
|
||||||
|
} else {
|
||||||
|
pcmEncoding = Format.NO_VALUE;
|
||||||
|
mimeType = MimeTypes.AUDIO_UNKNOWN;
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Unsupported floating point PCM bit depth: "
|
||||||
|
+ audioBitDepth
|
||||||
|
+ ". Setting mimeType to "
|
||||||
|
+ mimeType);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CODEC_ID_SUBRIP:
|
case CODEC_ID_SUBRIP:
|
||||||
|
@ -277,6 +277,9 @@ import java.util.List;
|
|||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_TTML = 0x54544d4c;
|
public static final int TYPE_TTML = 0x54544d4c;
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
|
public static final int TYPE_m1v_ = 0x6d317620;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_mp4v = 0x6d703476;
|
public static final int TYPE_mp4v = 0x6d703476;
|
||||||
|
|
||||||
|
@ -853,6 +853,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
if (childAtomType == Atom.TYPE_avc1
|
if (childAtomType == Atom.TYPE_avc1
|
||||||
|| childAtomType == Atom.TYPE_avc3
|
|| childAtomType == Atom.TYPE_avc3
|
||||||
|| childAtomType == Atom.TYPE_encv
|
|| childAtomType == Atom.TYPE_encv
|
||||||
|
|| childAtomType == Atom.TYPE_m1v_
|
||||||
|| childAtomType == Atom.TYPE_mp4v
|
|| childAtomType == Atom.TYPE_mp4v
|
||||||
|| childAtomType == Atom.TYPE_hvc1
|
|| childAtomType == Atom.TYPE_hvc1
|
||||||
|| childAtomType == Atom.TYPE_hev1
|
|| childAtomType == Atom.TYPE_hev1
|
||||||
@ -993,8 +994,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
// drmInitData = null;
|
// drmInitData = null;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@Nullable List<byte[]> initializationData = null;
|
|
||||||
@Nullable String mimeType = null;
|
@Nullable String mimeType = null;
|
||||||
|
if (atomType == Atom.TYPE_m1v_) {
|
||||||
|
mimeType = MimeTypes.VIDEO_MPEG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable List<byte[]> initializationData = null;
|
||||||
@Nullable String codecs = null;
|
@Nullable String codecs = null;
|
||||||
@Nullable byte[] projectionData = null;
|
@Nullable byte[] projectionData = null;
|
||||||
@C.StereoMode
|
@C.StereoMode
|
||||||
|
@ -199,10 +199,10 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
|||||||
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
||||||
// exist. If we know from the codec attribute that they don't exist, then we can
|
// exist. If we know from the codec attribute that they don't exist, then we can
|
||||||
// explicitly ignore them even if they're declared.
|
// explicitly ignore them even if they're declared.
|
||||||
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
|
if (!MimeTypes.containsCodecsCorrespondingToMimeType(codecs, MimeTypes.AUDIO_AAC)) {
|
||||||
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
|
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
|
||||||
}
|
}
|
||||||
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
|
if (!MimeTypes.containsCodecsCorrespondingToMimeType(codecs, MimeTypes.VIDEO_H264)) {
|
||||||
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
|
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -603,6 +603,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
String codecs = selectedPlaylistFormats[0].codecs;
|
String codecs = selectedPlaylistFormats[0].codecs;
|
||||||
|
int numberOfVideoCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_VIDEO);
|
||||||
|
int numberOfAudioCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_AUDIO);
|
||||||
|
boolean codecsStringAllowsChunklessPreparation =
|
||||||
|
numberOfAudioCodecs <= 1
|
||||||
|
&& numberOfVideoCodecs <= 1
|
||||||
|
&& numberOfAudioCodecs + numberOfVideoCodecs > 0;
|
||||||
HlsSampleStreamWrapper sampleStreamWrapper =
|
HlsSampleStreamWrapper sampleStreamWrapper =
|
||||||
buildSampleStreamWrapper(
|
buildSampleStreamWrapper(
|
||||||
C.TRACK_TYPE_DEFAULT,
|
C.TRACK_TYPE_DEFAULT,
|
||||||
@ -614,18 +620,16 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
positionUs);
|
positionUs);
|
||||||
sampleStreamWrappers.add(sampleStreamWrapper);
|
sampleStreamWrappers.add(sampleStreamWrapper);
|
||||||
manifestUrlIndicesPerWrapper.add(selectedVariantIndices);
|
manifestUrlIndicesPerWrapper.add(selectedVariantIndices);
|
||||||
if (allowChunklessPreparation && codecs != null) {
|
if (allowChunklessPreparation && codecsStringAllowsChunklessPreparation) {
|
||||||
boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;
|
|
||||||
boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null;
|
|
||||||
List<TrackGroup> muxedTrackGroups = new ArrayList<>();
|
List<TrackGroup> muxedTrackGroups = new ArrayList<>();
|
||||||
if (variantsContainVideoCodecs) {
|
if (numberOfVideoCodecs > 0) {
|
||||||
Format[] videoFormats = new Format[selectedVariantsCount];
|
Format[] videoFormats = new Format[selectedVariantsCount];
|
||||||
for (int i = 0; i < videoFormats.length; i++) {
|
for (int i = 0; i < videoFormats.length; i++) {
|
||||||
videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]);
|
videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]);
|
||||||
}
|
}
|
||||||
muxedTrackGroups.add(new TrackGroup(videoFormats));
|
muxedTrackGroups.add(new TrackGroup(videoFormats));
|
||||||
|
|
||||||
if (variantsContainAudioCodecs
|
if (numberOfAudioCodecs > 0
|
||||||
&& (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) {
|
&& (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) {
|
||||||
muxedTrackGroups.add(
|
muxedTrackGroups.add(
|
||||||
new TrackGroup(
|
new TrackGroup(
|
||||||
@ -640,7 +644,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
muxedTrackGroups.add(new TrackGroup(ccFormats.get(i)));
|
muxedTrackGroups.add(new TrackGroup(ccFormats.get(i)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (variantsContainAudioCodecs) {
|
} else /* numberOfAudioCodecs > 0 */ {
|
||||||
// Variants only contain audio.
|
// Variants only contain audio.
|
||||||
Format[] audioFormats = new Format[selectedVariantsCount];
|
Format[] audioFormats = new Format[selectedVariantsCount];
|
||||||
for (int i = 0; i < audioFormats.length; i++) {
|
for (int i = 0; i < audioFormats.length; i++) {
|
||||||
@ -651,9 +655,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
/* isPrimaryTrackInVariant= */ true);
|
/* isPrimaryTrackInVariant= */ true);
|
||||||
}
|
}
|
||||||
muxedTrackGroups.add(new TrackGroup(audioFormats));
|
muxedTrackGroups.add(new TrackGroup(audioFormats));
|
||||||
} else {
|
|
||||||
// Variants contain codecs but no video or audio entries could be identified.
|
|
||||||
throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackGroup id3TrackGroup =
|
TrackGroup id3TrackGroup =
|
||||||
@ -693,7 +694,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean renditionsHaveCodecs = true;
|
boolean codecStringsAllowChunklessPreparation = true;
|
||||||
scratchPlaylistUrls.clear();
|
scratchPlaylistUrls.clear();
|
||||||
scratchPlaylistFormats.clear();
|
scratchPlaylistFormats.clear();
|
||||||
scratchIndicesList.clear();
|
scratchIndicesList.clear();
|
||||||
@ -704,7 +705,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
scratchIndicesList.add(renditionIndex);
|
scratchIndicesList.add(renditionIndex);
|
||||||
scratchPlaylistUrls.add(rendition.url);
|
scratchPlaylistUrls.add(rendition.url);
|
||||||
scratchPlaylistFormats.add(rendition.format);
|
scratchPlaylistFormats.add(rendition.format);
|
||||||
renditionsHaveCodecs &= rendition.format.codecs != null;
|
codecStringsAllowChunklessPreparation &=
|
||||||
|
Util.getCodecCountOfType(rendition.format.codecs, C.TRACK_TYPE_AUDIO) == 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -720,7 +722,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||||||
manifestUrlsIndicesPerWrapper.add(Ints.toArray(scratchIndicesList));
|
manifestUrlsIndicesPerWrapper.add(Ints.toArray(scratchIndicesList));
|
||||||
sampleStreamWrappers.add(sampleStreamWrapper);
|
sampleStreamWrappers.add(sampleStreamWrapper);
|
||||||
|
|
||||||
if (allowChunklessPreparation && renditionsHaveCodecs) {
|
if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) {
|
||||||
Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]);
|
Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]);
|
||||||
sampleStreamWrapper.prepareWithMasterPlaylistInfo(
|
sampleStreamWrapper.prepareWithMasterPlaylistInfo(
|
||||||
new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0);
|
new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0);
|
||||||
|
@ -490,6 +490,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
loadingFinished = false;
|
loadingFinished = false;
|
||||||
mediaChunks.clear();
|
mediaChunks.clear();
|
||||||
if (loader.isLoading()) {
|
if (loader.isLoading()) {
|
||||||
|
if (sampleQueuesBuilt) {
|
||||||
|
// Discard as much as we can synchronously.
|
||||||
|
for (SampleQueue sampleQueue : sampleQueues) {
|
||||||
|
sampleQueue.discardToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
loader.cancelLoading();
|
loader.cancelLoading();
|
||||||
} else {
|
} else {
|
||||||
loader.clearFatalError();
|
loader.clearFatalError();
|
||||||
@ -1390,7 +1396,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives a track sample format from the corresponding format in the master playlist, and a
|
* Derives a track sample format from the corresponding format in the master playlist, and a
|
||||||
* sample format that may have been obtained from a chunk belonging to a different track.
|
* sample format that may have been obtained from a chunk belonging to a different track in the
|
||||||
|
* same track group.
|
||||||
*
|
*
|
||||||
* @param playlistFormat The format information obtained from the master playlist.
|
* @param playlistFormat The format information obtained from the master playlist.
|
||||||
* @param sampleFormat The format information obtained from the samples.
|
* @param sampleFormat The format information obtained from the samples.
|
||||||
@ -1405,8 +1412,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
}
|
}
|
||||||
|
|
||||||
int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
|
int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
|
||||||
@Nullable String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);
|
@Nullable String sampleMimeType;
|
||||||
@Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs);
|
@Nullable String codecs;
|
||||||
|
if (Util.getCodecCountOfType(playlistFormat.codecs, sampleTrackType) == 1) {
|
||||||
|
// We can unequivocally map this track to a playlist variant because only one codec string
|
||||||
|
// matches this track's type.
|
||||||
|
codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);
|
||||||
|
sampleMimeType = MimeTypes.getMediaMimeType(codecs);
|
||||||
|
} else {
|
||||||
|
// The variant assigns more than one codec string to this track. We choose whichever codec
|
||||||
|
// string matches the sample mime type. This can happen when different languages are encoded
|
||||||
|
// using different codecs.
|
||||||
|
codecs =
|
||||||
|
MimeTypes.getCodecsCorrespondingToMimeType(
|
||||||
|
playlistFormat.codecs, sampleFormat.sampleMimeType);
|
||||||
|
sampleMimeType = sampleFormat.sampleMimeType;
|
||||||
|
}
|
||||||
|
|
||||||
Format.Builder formatBuilder =
|
Format.Builder formatBuilder =
|
||||||
sampleFormat
|
sampleFormat
|
||||||
|
@ -152,6 +152,15 @@ public class DefaultTimeBar extends View implements TimeBar {
|
|||||||
/** Default color for played ad markers. */
|
/** Default color for played ad markers. */
|
||||||
public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00;
|
public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00;
|
||||||
|
|
||||||
|
// LINT.IfChange
|
||||||
|
/** Vertical gravity for progress bar to be located at the center in the view. */
|
||||||
|
public static final int BAR_GRAVITY_CENTER = 0;
|
||||||
|
/** Vertical gravity for progress bar to be located at the bottom in the view. */
|
||||||
|
public static final int BAR_GRAVITY_BOTTOM = 1;
|
||||||
|
/** Vertical gravity for progress bar to be located at the top in the view. */
|
||||||
|
public static final int BAR_GRAVITY_TOP = 2;
|
||||||
|
// LINT.ThenChange(../../../../../../../../../ui/src/main/res/values/attrs.xml)
|
||||||
|
|
||||||
/** The threshold in dps above the bar at which touch events trigger fine scrub mode. */
|
/** The threshold in dps above the bar at which touch events trigger fine scrub mode. */
|
||||||
private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50;
|
private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50;
|
||||||
/** The ratio by which times are reduced in fine scrub mode. */
|
/** The ratio by which times are reduced in fine scrub mode. */
|
||||||
@ -186,6 +195,7 @@ public class DefaultTimeBar extends View implements TimeBar {
|
|||||||
@Nullable private final Drawable scrubberDrawable;
|
@Nullable private final Drawable scrubberDrawable;
|
||||||
private final int barHeight;
|
private final int barHeight;
|
||||||
private final int touchTargetHeight;
|
private final int touchTargetHeight;
|
||||||
|
private final int barGravity;
|
||||||
private final int adMarkerWidth;
|
private final int adMarkerWidth;
|
||||||
private final int scrubberEnabledSize;
|
private final int scrubberEnabledSize;
|
||||||
private final int scrubberDisabledSize;
|
private final int scrubberDisabledSize;
|
||||||
@ -286,6 +296,7 @@ public class DefaultTimeBar extends View implements TimeBar {
|
|||||||
defaultBarHeight);
|
defaultBarHeight);
|
||||||
touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height,
|
touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height,
|
||||||
defaultTouchTargetHeight);
|
defaultTouchTargetHeight);
|
||||||
|
barGravity = a.getInt(R.styleable.DefaultTimeBar_bar_gravity, BAR_GRAVITY_CENTER);
|
||||||
adMarkerWidth = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_ad_marker_width,
|
adMarkerWidth = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_ad_marker_width,
|
||||||
defaultAdMarkerWidth);
|
defaultAdMarkerWidth);
|
||||||
scrubberEnabledSize = a.getDimensionPixelSize(
|
scrubberEnabledSize = a.getDimensionPixelSize(
|
||||||
@ -318,6 +329,7 @@ public class DefaultTimeBar extends View implements TimeBar {
|
|||||||
} else {
|
} else {
|
||||||
barHeight = defaultBarHeight;
|
barHeight = defaultBarHeight;
|
||||||
touchTargetHeight = defaultTouchTargetHeight;
|
touchTargetHeight = defaultTouchTargetHeight;
|
||||||
|
barGravity = BAR_GRAVITY_CENTER;
|
||||||
adMarkerWidth = defaultAdMarkerWidth;
|
adMarkerWidth = defaultAdMarkerWidth;
|
||||||
scrubberEnabledSize = defaultScrubberEnabledSize;
|
scrubberEnabledSize = defaultScrubberEnabledSize;
|
||||||
scrubberDisabledSize = defaultScrubberDisabledSize;
|
scrubberDisabledSize = defaultScrubberDisabledSize;
|
||||||
@ -659,7 +671,14 @@ public class DefaultTimeBar extends View implements TimeBar {
|
|||||||
int barY = (height - touchTargetHeight) / 2;
|
int barY = (height - touchTargetHeight) / 2;
|
||||||
int seekLeft = getPaddingLeft();
|
int seekLeft = getPaddingLeft();
|
||||||
int seekRight = width - getPaddingRight();
|
int seekRight = width - getPaddingRight();
|
||||||
int progressY = barY + (touchTargetHeight - barHeight) / 2;
|
int progressY;
|
||||||
|
if (barGravity == BAR_GRAVITY_BOTTOM) {
|
||||||
|
progressY = barY + touchTargetHeight - (getPaddingBottom() + scrubberPadding + barHeight / 2);
|
||||||
|
} else if (barGravity == BAR_GRAVITY_TOP) {
|
||||||
|
progressY = barY + getPaddingTop() + scrubberPadding - barHeight / 2;
|
||||||
|
} else {
|
||||||
|
progressY = barY + (touchTargetHeight - barHeight) / 2;
|
||||||
|
}
|
||||||
seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight);
|
seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight);
|
||||||
progressBar.set(seekBounds.left + scrubberPadding, progressY,
|
progressBar.set(seekBounds.left + scrubberPadding, progressY,
|
||||||
seekBounds.right - scrubberPadding, progressY + barHeight);
|
seekBounds.right - scrubberPadding, progressY + barHeight);
|
||||||
|
@ -611,11 +611,15 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link PlaybackPreparer}.
|
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
|
||||||
*
|
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
|
||||||
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
|
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
|
||||||
* preparer.
|
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
|
||||||
|
* you can provide a custom implementation of {@link
|
||||||
|
* ControlDispatcher#dispatchPrepare(Player)}.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
||||||
this.playbackPreparer = playbackPreparer;
|
this.playbackPreparer = playbackPreparer;
|
||||||
}
|
}
|
||||||
@ -1254,11 +1258,14 @@ public class PlayerControlView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
private void dispatchPlay(Player player) {
|
private void dispatchPlay(Player player) {
|
||||||
@State int state = player.getPlaybackState();
|
@State int state = player.getPlaybackState();
|
||||||
if (state == Player.STATE_IDLE) {
|
if (state == Player.STATE_IDLE) {
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
playbackPreparer.preparePlayback();
|
playbackPreparer.preparePlayback();
|
||||||
|
} else {
|
||||||
|
controlDispatcher.dispatchPrepare(player);
|
||||||
}
|
}
|
||||||
} else if (state == Player.STATE_ENDED) {
|
} else if (state == Player.STATE_ENDED) {
|
||||||
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||||
|
@ -57,7 +57,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts, updates and cancels a media style notification reflecting the player state. The actions
|
* Starts, updates and cancels a media style notification reflecting the player state. The actions
|
||||||
* displayed and the drawables used can both be customized, as described below.
|
* included in the notification can be customized along with their drawables, as described below.
|
||||||
*
|
*
|
||||||
* <p>The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or
|
* <p>The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or
|
||||||
* when the notification is dismissed by the user.
|
* when the notification is dismissed by the user.
|
||||||
@ -67,43 +67,55 @@ import java.util.Map;
|
|||||||
*
|
*
|
||||||
* <h3>Action customization</h3>
|
* <h3>Action customization</h3>
|
||||||
*
|
*
|
||||||
* Playback actions can be displayed or omitted as follows:
|
* Playback actions can be included or omitted as follows:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li><b>{@code useNavigationActions}</b> - Sets whether the previous and next actions are
|
* <li><b>{@code usePlayPauseActions}</b> - Sets whether the play and pause actions are used.
|
||||||
* displayed.
|
|
||||||
* <ul>
|
|
||||||
* <li>Corresponding setter: {@link #setUseNavigationActions(boolean)}
|
|
||||||
* <li>Default: {@code true}
|
|
||||||
* </ul>
|
|
||||||
* <li><b>{@code useNavigationActionsInCompactView}</b> - Sets whether the previous and next
|
|
||||||
* actions are displayed in compact view (including the lock screen notification).
|
|
||||||
* <ul>
|
|
||||||
* <li>Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)}
|
|
||||||
* <li>Default: {@code false}
|
|
||||||
* </ul>
|
|
||||||
* <li><b>{@code usePlayPauseActions}</b> - Sets whether the play and pause actions are displayed.
|
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Corresponding setter: {@link #setUsePlayPauseActions(boolean)}
|
* <li>Corresponding setter: {@link #setUsePlayPauseActions(boolean)}
|
||||||
* <li>Default: {@code true}
|
* <li>Default: {@code true}
|
||||||
* </ul>
|
* </ul>
|
||||||
* <li><b>{@code useStopAction}</b> - Sets whether the stop action is displayed.
|
|
||||||
* <ul>
|
|
||||||
* <li>Corresponding setter: {@link #setUseStopAction(boolean)}
|
|
||||||
* <li>Default: {@code false}
|
|
||||||
* </ul>
|
|
||||||
* <li><b>{@code rewindIncrementMs}</b> - Sets the rewind increment. If set to zero the rewind
|
* <li><b>{@code rewindIncrementMs}</b> - Sets the rewind increment. If set to zero the rewind
|
||||||
* action is not displayed.
|
* action is not used.
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)}
|
* <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)}
|
||||||
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS} (5000)
|
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS} (5000)
|
||||||
* </ul>
|
* </ul>
|
||||||
* <li><b>{@code fastForwardIncrementMs}</b> - Sets the fast forward increment. If set to zero the
|
* <li><b>{@code fastForwardIncrementMs}</b> - Sets the fast forward increment. If set to zero the
|
||||||
* fast forward action is not displayed.
|
* fast forward action is not used.
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)}
|
* <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)}
|
||||||
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS} (15000)
|
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS} (15000)
|
||||||
* </ul>
|
* </ul>
|
||||||
|
* <li><b>{@code usePreviousAction}</b> - Whether the previous action is used.
|
||||||
|
* <ul>
|
||||||
|
* <li>Corresponding setter: {@link #setUsePreviousAction(boolean)}
|
||||||
|
* <li>Default: {@code true}
|
||||||
|
* </ul>
|
||||||
|
* <li><b>{@code usePreviousActionInCompactView}</b> - If {@code usePreviousAction} is {@code
|
||||||
|
* true}, sets whether the previous action is also used in compact view (including the lock
|
||||||
|
* screen notification). Else does nothing.
|
||||||
|
* <ul>
|
||||||
|
* <li>Corresponding setter: {@link #setUsePreviousActionInCompactView(boolean)}
|
||||||
|
* <li>Default: {@code false}
|
||||||
|
* </ul>
|
||||||
|
* <li><b>{@code useNextAction}</b> - Whether the next action is used.
|
||||||
|
* <ul>
|
||||||
|
* <li>Corresponding setter: {@link #setUseNextAction(boolean)}
|
||||||
|
* <li>Default: {@code true}
|
||||||
|
* </ul>
|
||||||
|
* <li><b>{@code useNextActionInCompactView}</b> - If {@code useNextAction} is {@code true}, sets
|
||||||
|
* whether the next action is also used in compact view (including the lock screen
|
||||||
|
* notification). Else does nothing.
|
||||||
|
* <ul>
|
||||||
|
* <li>Corresponding setter: {@link #setUseNextActionInCompactView(boolean)}
|
||||||
|
* <li>Default: {@code false}
|
||||||
|
* </ul>
|
||||||
|
* <li><b>{@code useStopAction}</b> - Sets whether the stop action is used.
|
||||||
|
* <ul>
|
||||||
|
* <li>Corresponding setter: {@link #setUseStopAction(boolean)}
|
||||||
|
* <li>Default: {@code false}
|
||||||
|
* </ul>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <h3>Overriding drawables</h3>
|
* <h3>Overriding drawables</h3>
|
||||||
@ -382,8 +394,10 @@ public class PlayerNotificationManager {
|
|||||||
private int currentNotificationTag;
|
private int currentNotificationTag;
|
||||||
@Nullable private NotificationListener notificationListener;
|
@Nullable private NotificationListener notificationListener;
|
||||||
@Nullable private MediaSessionCompat.Token mediaSessionToken;
|
@Nullable private MediaSessionCompat.Token mediaSessionToken;
|
||||||
private boolean useNavigationActions;
|
private boolean usePreviousAction;
|
||||||
private boolean useNavigationActionsInCompactView;
|
private boolean useNextAction;
|
||||||
|
private boolean usePreviousActionInCompactView;
|
||||||
|
private boolean useNextActionInCompactView;
|
||||||
private boolean usePlayPauseActions;
|
private boolean usePlayPauseActions;
|
||||||
private boolean useStopAction;
|
private boolean useStopAction;
|
||||||
private int badgeIconType;
|
private int badgeIconType;
|
||||||
@ -610,15 +624,18 @@ public class PlayerNotificationManager {
|
|||||||
controlDispatcher = new DefaultControlDispatcher();
|
controlDispatcher = new DefaultControlDispatcher();
|
||||||
window = new Timeline.Window();
|
window = new Timeline.Window();
|
||||||
instanceId = instanceIdCounter++;
|
instanceId = instanceIdCounter++;
|
||||||
//noinspection Convert2MethodRef
|
// This fails the nullness checker because handleMessage() is 'called' while `this` is still
|
||||||
mainHandler =
|
// @UnderInitialization. No tasks are scheduled on mainHandler before the constructor completes,
|
||||||
Util.createHandler(
|
// so this is safe and we can suppress the warning.
|
||||||
Looper.getMainLooper(), msg -> PlayerNotificationManager.this.handleMessage(msg));
|
@SuppressWarnings("nullness:methodref.receiver.bound.invalid")
|
||||||
|
Handler mainHandler = Util.createHandler(Looper.getMainLooper(), this::handleMessage);
|
||||||
|
this.mainHandler = mainHandler;
|
||||||
notificationManager = NotificationManagerCompat.from(context);
|
notificationManager = NotificationManagerCompat.from(context);
|
||||||
playerListener = new PlayerListener();
|
playerListener = new PlayerListener();
|
||||||
notificationBroadcastReceiver = new NotificationBroadcastReceiver();
|
notificationBroadcastReceiver = new NotificationBroadcastReceiver();
|
||||||
intentFilter = new IntentFilter();
|
intentFilter = new IntentFilter();
|
||||||
useNavigationActions = true;
|
usePreviousAction = true;
|
||||||
|
useNextAction = true;
|
||||||
usePlayPauseActions = true;
|
usePlayPauseActions = true;
|
||||||
colorized = true;
|
colorized = true;
|
||||||
useChronometer = true;
|
useChronometer = true;
|
||||||
@ -680,10 +697,16 @@ public class PlayerNotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link PlaybackPreparer}.
|
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The manager calls
|
||||||
*
|
* {@link ControlDispatcher#dispatchPrepare(Player)} instead of {@link
|
||||||
* @param playbackPreparer The {@link PlaybackPreparer}.
|
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that this manager
|
||||||
|
* uses by default, calls {@link Player#prepare()}. If you wish to intercept or customize this
|
||||||
|
* behaviour, you can provide a custom implementation of {@link
|
||||||
|
* ControlDispatcher#dispatchPrepare(Player)} and pass it to {@link
|
||||||
|
* #setControlDispatcher(ControlDispatcher)}.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
||||||
this.playbackPreparer = playbackPreparer;
|
this.playbackPreparer = playbackPreparer;
|
||||||
}
|
}
|
||||||
@ -742,34 +765,85 @@ public class PlayerNotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether the navigation actions should be used.
|
* Sets whether the next action should be used.
|
||||||
*
|
*
|
||||||
* @param useNavigationActions Whether to use navigation actions or not.
|
* @param useNextAction Whether to use the next action.
|
||||||
*/
|
*/
|
||||||
public final void setUseNavigationActions(boolean useNavigationActions) {
|
public void setUseNextAction(boolean useNextAction) {
|
||||||
if (this.useNavigationActions != useNavigationActions) {
|
if (this.useNextAction != useNextAction) {
|
||||||
this.useNavigationActions = useNavigationActions;
|
this.useNextAction = useNextAction;
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether navigation actions should be displayed in compact view.
|
* Sets whether the previous action should be used.
|
||||||
*
|
*
|
||||||
* <p>If {@link #useNavigationActions} is set to {@code false} navigation actions are displayed
|
* @param usePreviousAction Whether to use the previous action.
|
||||||
* neither in compact nor in full view mode of the notification.
|
|
||||||
*
|
|
||||||
* @param useNavigationActionsInCompactView Whether the navigation actions should be displayed in
|
|
||||||
* compact view.
|
|
||||||
*/
|
*/
|
||||||
public final void setUseNavigationActionsInCompactView(
|
public void setUsePreviousAction(boolean usePreviousAction) {
|
||||||
boolean useNavigationActionsInCompactView) {
|
if (this.usePreviousAction != usePreviousAction) {
|
||||||
if (this.useNavigationActionsInCompactView != useNavigationActionsInCompactView) {
|
this.usePreviousAction = usePreviousAction;
|
||||||
this.useNavigationActionsInCompactView = useNavigationActionsInCompactView;
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the navigation actions should be used.
|
||||||
|
*
|
||||||
|
* @param useNavigationActions Whether to use navigation actions.
|
||||||
|
* @deprecated Use {@link #setUseNextAction(boolean)} and {@link #setUsePreviousAction(boolean)}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public final void setUseNavigationActions(boolean useNavigationActions) {
|
||||||
|
setUseNextAction(useNavigationActions);
|
||||||
|
setUsePreviousAction(useNavigationActions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If {@link #setUseNextAction useNextAction} is {@code true}, sets whether the next action should
|
||||||
|
* also be used in compact view. Has no effect if {@link #setUseNextAction useNextAction} is
|
||||||
|
* {@code false}.
|
||||||
|
*
|
||||||
|
* @param useNextActionInCompactView Whether to use the next action in compact view.
|
||||||
|
*/
|
||||||
|
public void setUseNextActionInCompactView(boolean useNextActionInCompactView) {
|
||||||
|
if (this.useNextActionInCompactView != useNextActionInCompactView) {
|
||||||
|
this.useNextActionInCompactView = useNextActionInCompactView;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If {@link #setUsePreviousAction usePreviousAction} is {@code true}, sets whether the previous
|
||||||
|
* action should also be used in compact view. Has no effect if {@link #setUsePreviousAction
|
||||||
|
* usePreviousAction} is {@code false}.
|
||||||
|
*
|
||||||
|
* @param usePreviousActionInCompactView Whether to use the previous action in compact view.
|
||||||
|
*/
|
||||||
|
public void setUsePreviousActionInCompactView(boolean usePreviousActionInCompactView) {
|
||||||
|
if (this.usePreviousActionInCompactView != usePreviousActionInCompactView) {
|
||||||
|
this.usePreviousActionInCompactView = usePreviousActionInCompactView;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If {@link #setUseNavigationActions useNavigationActions} is {@code true}, sets whether
|
||||||
|
* navigation actions should also be used in compact view. Has no effect if {@link
|
||||||
|
* #setUseNavigationActions useNavigationActions} is {@code false}.
|
||||||
|
*
|
||||||
|
* @param useNavigationActionsInCompactView Whether to use navigation actions in compact view.
|
||||||
|
* @deprecated Use {@link #setUseNextActionInCompactView(boolean)} and {@link
|
||||||
|
* #setUsePreviousActionInCompactView(boolean)} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public final void setUseNavigationActionsInCompactView(
|
||||||
|
boolean useNavigationActionsInCompactView) {
|
||||||
|
setUseNextActionInCompactView(useNavigationActionsInCompactView);
|
||||||
|
setUsePreviousActionInCompactView(useNavigationActionsInCompactView);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether the play and pause actions should be used.
|
* Sets whether the play and pause actions should be used.
|
||||||
*
|
*
|
||||||
@ -1037,8 +1111,7 @@ public class PlayerNotificationManager {
|
|||||||
@Nullable NotificationCompat.Builder builder,
|
@Nullable NotificationCompat.Builder builder,
|
||||||
boolean ongoing,
|
boolean ongoing,
|
||||||
@Nullable Bitmap largeIcon) {
|
@Nullable Bitmap largeIcon) {
|
||||||
if (player.getPlaybackState() == Player.STATE_IDLE
|
if (player.getPlaybackState() == Player.STATE_IDLE && player.getCurrentTimeline().isEmpty()) {
|
||||||
&& (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) {
|
|
||||||
builderActions = null;
|
builderActions = null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -1153,7 +1226,7 @@ public class PlayerNotificationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> stringActions = new ArrayList<>();
|
List<String> stringActions = new ArrayList<>();
|
||||||
if (useNavigationActions && enablePrevious) {
|
if (usePreviousAction && enablePrevious) {
|
||||||
stringActions.add(ACTION_PREVIOUS);
|
stringActions.add(ACTION_PREVIOUS);
|
||||||
}
|
}
|
||||||
if (enableRewind) {
|
if (enableRewind) {
|
||||||
@ -1169,7 +1242,7 @@ public class PlayerNotificationManager {
|
|||||||
if (enableFastForward) {
|
if (enableFastForward) {
|
||||||
stringActions.add(ACTION_FAST_FORWARD);
|
stringActions.add(ACTION_FAST_FORWARD);
|
||||||
}
|
}
|
||||||
if (useNavigationActions && enableNext) {
|
if (useNextAction && enableNext) {
|
||||||
stringActions.add(ACTION_NEXT);
|
stringActions.add(ACTION_NEXT);
|
||||||
}
|
}
|
||||||
if (customActionReceiver != null) {
|
if (customActionReceiver != null) {
|
||||||
@ -1194,15 +1267,14 @@ public class PlayerNotificationManager {
|
|||||||
protected int[] getActionIndicesForCompactView(List<String> actionNames, Player player) {
|
protected int[] getActionIndicesForCompactView(List<String> actionNames, Player player) {
|
||||||
int pauseActionIndex = actionNames.indexOf(ACTION_PAUSE);
|
int pauseActionIndex = actionNames.indexOf(ACTION_PAUSE);
|
||||||
int playActionIndex = actionNames.indexOf(ACTION_PLAY);
|
int playActionIndex = actionNames.indexOf(ACTION_PLAY);
|
||||||
int skipPreviousActionIndex =
|
int previousActionIndex =
|
||||||
useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_PREVIOUS) : -1;
|
usePreviousActionInCompactView ? actionNames.indexOf(ACTION_PREVIOUS) : -1;
|
||||||
int skipNextActionIndex =
|
int nextActionIndex = useNextActionInCompactView ? actionNames.indexOf(ACTION_NEXT) : -1;
|
||||||
useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_NEXT) : -1;
|
|
||||||
|
|
||||||
int[] actionIndices = new int[3];
|
int[] actionIndices = new int[3];
|
||||||
int actionCounter = 0;
|
int actionCounter = 0;
|
||||||
if (skipPreviousActionIndex != -1) {
|
if (previousActionIndex != -1) {
|
||||||
actionIndices[actionCounter++] = skipPreviousActionIndex;
|
actionIndices[actionCounter++] = previousActionIndex;
|
||||||
}
|
}
|
||||||
boolean shouldShowPauseButton = shouldShowPauseButton(player);
|
boolean shouldShowPauseButton = shouldShowPauseButton(player);
|
||||||
if (pauseActionIndex != -1 && shouldShowPauseButton) {
|
if (pauseActionIndex != -1 && shouldShowPauseButton) {
|
||||||
@ -1210,8 +1282,8 @@ public class PlayerNotificationManager {
|
|||||||
} else if (playActionIndex != -1 && !shouldShowPauseButton) {
|
} else if (playActionIndex != -1 && !shouldShowPauseButton) {
|
||||||
actionIndices[actionCounter++] = playActionIndex;
|
actionIndices[actionCounter++] = playActionIndex;
|
||||||
}
|
}
|
||||||
if (skipNextActionIndex != -1) {
|
if (nextActionIndex != -1) {
|
||||||
actionIndices[actionCounter++] = skipNextActionIndex;
|
actionIndices[actionCounter++] = nextActionIndex;
|
||||||
}
|
}
|
||||||
return Arrays.copyOf(actionIndices, actionCounter);
|
return Arrays.copyOf(actionIndices, actionCounter);
|
||||||
}
|
}
|
||||||
@ -1367,6 +1439,7 @@ public class PlayerNotificationManager {
|
|||||||
|
|
||||||
private class NotificationBroadcastReceiver extends BroadcastReceiver {
|
private class NotificationBroadcastReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
Player player = PlayerNotificationManager.this.player;
|
Player player = PlayerNotificationManager.this.player;
|
||||||
@ -1380,6 +1453,8 @@ public class PlayerNotificationManager {
|
|||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
playbackPreparer.preparePlayback();
|
playbackPreparer.preparePlayback();
|
||||||
|
} else {
|
||||||
|
controlDispatcher.dispatchPrepare(player);
|
||||||
}
|
}
|
||||||
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
|
||||||
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||||
|
@ -983,11 +983,15 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link PlaybackPreparer}.
|
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
|
||||||
*
|
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
|
||||||
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
|
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
|
||||||
* preparer.
|
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
|
||||||
|
* you can provide a custom implementation of {@link
|
||||||
|
* ControlDispatcher#dispatchPrepare(Player)}.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
||||||
Assertions.checkStateNotNull(controller);
|
Assertions.checkStateNotNull(controller);
|
||||||
controller.setPlaybackPreparer(playbackPreparer);
|
controller.setPlaybackPreparer(playbackPreparer);
|
||||||
|
@ -834,11 +834,15 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link PlaybackPreparer}.
|
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
|
||||||
*
|
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
|
||||||
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
|
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
|
||||||
* preparer.
|
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
|
||||||
|
* you can provide a custom implementation of {@link
|
||||||
|
* ControlDispatcher#dispatchPrepare(Player)}.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
||||||
this.playbackPreparer = playbackPreparer;
|
this.playbackPreparer = playbackPreparer;
|
||||||
}
|
}
|
||||||
@ -1698,11 +1702,14 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
private void dispatchPlay(Player player) {
|
private void dispatchPlay(Player player) {
|
||||||
@State int state = player.getPlaybackState();
|
@State int state = player.getPlaybackState();
|
||||||
if (state == Player.STATE_IDLE) {
|
if (state == Player.STATE_IDLE) {
|
||||||
if (playbackPreparer != null) {
|
if (playbackPreparer != null) {
|
||||||
playbackPreparer.preparePlayback();
|
playbackPreparer.preparePlayback();
|
||||||
|
} else {
|
||||||
|
controlDispatcher.dispatchPrepare(player);
|
||||||
}
|
}
|
||||||
} else if (state == Player.STATE_ENDED) {
|
} else if (state == Player.STATE_ENDED) {
|
||||||
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
|
||||||
@ -1920,7 +1927,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SettingViewHolder extends RecyclerView.ViewHolder {
|
private final class SettingViewHolder extends RecyclerView.ViewHolder {
|
||||||
private final TextView mainTextView;
|
private final TextView mainTextView;
|
||||||
private final TextView subTextView;
|
private final TextView subTextView;
|
||||||
private final ImageView iconView;
|
private final ImageView iconView;
|
||||||
@ -1930,8 +1937,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
mainTextView = itemView.findViewById(R.id.exo_main_text);
|
mainTextView = itemView.findViewById(R.id.exo_main_text);
|
||||||
subTextView = itemView.findViewById(R.id.exo_sub_text);
|
subTextView = itemView.findViewById(R.id.exo_sub_text);
|
||||||
iconView = itemView.findViewById(R.id.exo_icon);
|
iconView = itemView.findViewById(R.id.exo_icon);
|
||||||
itemView.setOnClickListener(
|
itemView.setOnClickListener(v -> onSettingViewClicked(getAdapterPosition()));
|
||||||
v -> onSettingViewClicked(SettingViewHolder.this.getAdapterPosition()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1969,7 +1975,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SubSettingViewHolder extends RecyclerView.ViewHolder {
|
private final class SubSettingViewHolder extends RecyclerView.ViewHolder {
|
||||||
private final TextView textView;
|
private final TextView textView;
|
||||||
private final View checkView;
|
private final View checkView;
|
||||||
|
|
||||||
@ -1977,8 +1983,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
super(itemView);
|
super(itemView);
|
||||||
textView = itemView.findViewById(R.id.exo_text);
|
textView = itemView.findViewById(R.id.exo_text);
|
||||||
checkView = itemView.findViewById(R.id.exo_check);
|
checkView = itemView.findViewById(R.id.exo_check);
|
||||||
itemView.setOnClickListener(
|
itemView.setOnClickListener(v -> onSubSettingViewClicked(getAdapterPosition()));
|
||||||
v -> onSubSettingViewClicked(SubSettingViewHolder.this.getAdapterPosition()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ import androidx.annotation.RequiresApi;
|
|||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ControlDispatcher;
|
import com.google.android.exoplayer2.ControlDispatcher;
|
||||||
|
import com.google.android.exoplayer2.DefaultControlDispatcher;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.PlaybackPreparer;
|
import com.google.android.exoplayer2.PlaybackPreparer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
@ -978,11 +979,15 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link PlaybackPreparer}.
|
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
|
||||||
*
|
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
|
||||||
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
|
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
|
||||||
* preparer.
|
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
|
||||||
|
* you can provide a custom implementation of {@link
|
||||||
|
* ControlDispatcher#dispatchPrepare(Player)}.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
|
||||||
Assertions.checkStateNotNull(controller);
|
Assertions.checkStateNotNull(controller);
|
||||||
controller.setPlaybackPreparer(playbackPreparer);
|
controller.setPlaybackPreparer(playbackPreparer);
|
||||||
|
@ -289,10 +289,10 @@ public class TrackSelectionView extends LinearLayout {
|
|||||||
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
|
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
|
||||||
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
|
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
|
||||||
trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format));
|
trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format));
|
||||||
|
trackView.setTag(trackInfos[trackIndex]);
|
||||||
if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
|
if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
|
||||||
== RendererCapabilities.FORMAT_HANDLED) {
|
== RendererCapabilities.FORMAT_HANDLED) {
|
||||||
trackView.setFocusable(true);
|
trackView.setFocusable(true);
|
||||||
trackView.setTag(trackInfos[trackIndex]);
|
|
||||||
trackView.setOnClickListener(componentListener);
|
trackView.setOnClickListener(componentListener);
|
||||||
} else {
|
} else {
|
||||||
trackView.setFocusable(false);
|
trackView.setFocusable(false);
|
||||||
|
@ -54,8 +54,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
private @MonotonicNonNull SurfaceTexture surfaceTexture;
|
private @MonotonicNonNull SurfaceTexture surfaceTexture;
|
||||||
|
|
||||||
// Used by other threads only
|
// Used by other threads only
|
||||||
private volatile @C.StreamType int defaultStereoMode;
|
@C.StereoMode private volatile int defaultStereoMode;
|
||||||
private @C.StreamType int lastStereoMode;
|
@C.StereoMode private int lastStereoMode;
|
||||||
@Nullable private byte[] lastProjectionData;
|
@Nullable private byte[] lastProjectionData;
|
||||||
|
|
||||||
// Methods called on any thread.
|
// Methods called on any thread.
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
android:minWidth="@dimen/exo_setting_width"
|
android:minWidth="@dimen/exo_setting_width"
|
||||||
android:minHeight="@dimen/exo_settings_height"
|
android:minHeight="@dimen/exo_settings_height"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:layoutDirection="locale"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@ -38,6 +39,7 @@
|
|||||||
android:paddingEnd="4dp"
|
android:paddingEnd="4dp"
|
||||||
android:paddingRight="4dp"
|
android:paddingRight="4dp"
|
||||||
android:gravity="center|start"
|
android:gravity="center|start"
|
||||||
|
android:layoutDirection="locale"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -45,6 +47,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/exo_white"
|
android:textColor="@color/exo_white"
|
||||||
|
android:textDirection="locale"
|
||||||
android:textSize="@dimen/exo_settings_main_text_size"/>
|
android:textSize="@dimen/exo_settings_main_text_size"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -52,6 +55,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/exo_white_opacity_70"
|
android:textColor="@color/exo_white_opacity_70"
|
||||||
|
android:textDirection="locale"
|
||||||
android:textSize="@dimen/exo_settings_sub_text_size"/>
|
android:textSize="@dimen/exo_settings_sub_text_size"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
android:minWidth="@dimen/exo_setting_width"
|
android:minWidth="@dimen/exo_setting_width"
|
||||||
android:minHeight="@dimen/exo_settings_height"
|
android:minHeight="@dimen/exo_settings_height"
|
||||||
android:background="?android:attr/selectableItemBackground"
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:layoutDirection="locale"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
@ -40,5 +41,6 @@
|
|||||||
android:layout_marginRight="4dp"
|
android:layout_marginRight="4dp"
|
||||||
android:gravity="center|start"
|
android:gravity="center|start"
|
||||||
android:textColor="@color/exo_white"
|
android:textColor="@color/exo_white"
|
||||||
|
android:textDirection="locale"
|
||||||
android:textSize="@dimen/exo_settings_main_text_size"/>
|
android:textSize="@dimen/exo_settings_main_text_size"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -72,8 +72,16 @@
|
|||||||
<attr name="controller_layout_id" format="reference"/>
|
<attr name="controller_layout_id" format="reference"/>
|
||||||
<attr name="animation_enabled" format="boolean"/>
|
<attr name="animation_enabled" format="boolean"/>
|
||||||
|
|
||||||
|
<!-- Needed for https://github.com/google/ExoPlayer/issues/7898 -->
|
||||||
|
<attr name="backgroundTint" format="color"/>
|
||||||
|
|
||||||
<!-- DefaultTimeBar attributes -->
|
<!-- DefaultTimeBar attributes -->
|
||||||
<attr name="bar_height" format="dimension"/>
|
<attr name="bar_height" format="dimension"/>
|
||||||
|
<attr name="bar_gravity" format="enum">
|
||||||
|
<enum name="center" value="0"/>
|
||||||
|
<enum name="bottom" value="1"/>
|
||||||
|
<enum name="top" value="2"/>
|
||||||
|
</attr>
|
||||||
<attr name="touch_target_height" format="dimension"/>
|
<attr name="touch_target_height" format="dimension"/>
|
||||||
<attr name="ad_marker_width" format="dimension"/>
|
<attr name="ad_marker_width" format="dimension"/>
|
||||||
<attr name="scrubber_enabled_size" format="dimension"/>
|
<attr name="scrubber_enabled_size" format="dimension"/>
|
||||||
@ -154,6 +162,7 @@
|
|||||||
<attr name="animation_enabled"/>
|
<attr name="animation_enabled"/>
|
||||||
<!-- DefaultTimeBar attributes -->
|
<!-- DefaultTimeBar attributes -->
|
||||||
<attr name="bar_height"/>
|
<attr name="bar_height"/>
|
||||||
|
<attr name="bar_gravity"/>
|
||||||
<attr name="touch_target_height"/>
|
<attr name="touch_target_height"/>
|
||||||
<attr name="ad_marker_width"/>
|
<attr name="ad_marker_width"/>
|
||||||
<attr name="scrubber_enabled_size"/>
|
<attr name="scrubber_enabled_size"/>
|
||||||
@ -186,6 +195,7 @@
|
|||||||
<attr name="controller_layout_id"/>
|
<attr name="controller_layout_id"/>
|
||||||
<!-- DefaultTimeBar attributes -->
|
<!-- DefaultTimeBar attributes -->
|
||||||
<attr name="bar_height"/>
|
<attr name="bar_height"/>
|
||||||
|
<attr name="bar_gravity"/>
|
||||||
<attr name="touch_target_height"/>
|
<attr name="touch_target_height"/>
|
||||||
<attr name="ad_marker_width"/>
|
<attr name="ad_marker_width"/>
|
||||||
<attr name="scrubber_enabled_size"/>
|
<attr name="scrubber_enabled_size"/>
|
||||||
@ -217,6 +227,7 @@
|
|||||||
<attr name="animation_enabled"/>
|
<attr name="animation_enabled"/>
|
||||||
<!-- DefaultTimeBar attributes -->
|
<!-- DefaultTimeBar attributes -->
|
||||||
<attr name="bar_height"/>
|
<attr name="bar_height"/>
|
||||||
|
<attr name="bar_gravity"/>
|
||||||
<attr name="touch_target_height"/>
|
<attr name="touch_target_height"/>
|
||||||
<attr name="ad_marker_width"/>
|
<attr name="ad_marker_width"/>
|
||||||
<attr name="scrubber_enabled_size"/>
|
<attr name="scrubber_enabled_size"/>
|
||||||
@ -233,6 +244,7 @@
|
|||||||
|
|
||||||
<declare-styleable name="DefaultTimeBar">
|
<declare-styleable name="DefaultTimeBar">
|
||||||
<attr name="bar_height"/>
|
<attr name="bar_height"/>
|
||||||
|
<attr name="bar_gravity"/>
|
||||||
<attr name="touch_target_height"/>
|
<attr name="touch_target_height"/>
|
||||||
<attr name="ad_marker_width"/>
|
<attr name="ad_marker_width"/>
|
||||||
<attr name="scrubber_enabled_size"/>
|
<attr name="scrubber_enabled_size"/>
|
||||||
|
@ -38,8 +38,8 @@
|
|||||||
<dimen name="exo_styled_progress_bar_height">2dp</dimen>
|
<dimen name="exo_styled_progress_bar_height">2dp</dimen>
|
||||||
<dimen name="exo_styled_progress_enabled_thumb_size">10dp</dimen>
|
<dimen name="exo_styled_progress_enabled_thumb_size">10dp</dimen>
|
||||||
<dimen name="exo_styled_progress_dragged_thumb_size">14dp</dimen>
|
<dimen name="exo_styled_progress_dragged_thumb_size">14dp</dimen>
|
||||||
<dimen name="exo_styled_progress_layout_height">14dp</dimen>
|
<dimen name="exo_styled_progress_layout_height">48dp</dimen>
|
||||||
<dimen name="exo_styled_progress_touch_target_height">14dp</dimen>
|
<dimen name="exo_styled_progress_touch_target_height">48dp</dimen>
|
||||||
<dimen name="exo_styled_progress_margin_bottom">52dp</dimen>
|
<dimen name="exo_styled_progress_margin_bottom">52dp</dimen>
|
||||||
|
|
||||||
<dimen name="exo_bottom_bar_height">60dp</dimen>
|
<dimen name="exo_bottom_bar_height">60dp</dimen>
|
||||||
|
@ -93,12 +93,19 @@
|
|||||||
<item name="android:gravity">center|bottom</item>
|
<item name="android:gravity">center|bottom</item>
|
||||||
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
|
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
|
||||||
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
|
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
|
||||||
|
<!-- Needed for https://github.com/google/ExoPlayer/issues/7898 -->
|
||||||
|
<item name="backgroundTint">@android:color/white</item>
|
||||||
|
<item name="android:insetBottom">0dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="ExoStyledControls.Button.Center.RewWithAmount">
|
<style name="ExoStyledControls.Button.Center.RewWithAmount">
|
||||||
<item name="android:background">@drawable/exo_ripple_rew</item>
|
<item name="android:background">@drawable/exo_ripple_rew</item>
|
||||||
<item name="android:gravity">center|bottom</item>
|
<item name="android:gravity">center|bottom</item>
|
||||||
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
|
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
|
||||||
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
|
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
|
||||||
|
<!-- Needed for https://github.com/google/ExoPlayer/issues/7898 -->
|
||||||
|
<item name="backgroundTint">@android:color/white</item>
|
||||||
|
<item name="android:insetBottom">0dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="ExoStyledControls.ButtonText">
|
<style name="ExoStyledControls.ButtonText">
|
||||||
@ -187,6 +194,7 @@
|
|||||||
|
|
||||||
<style name="ExoStyledControls.TimeBar">
|
<style name="ExoStyledControls.TimeBar">
|
||||||
<item name="bar_height">@dimen/exo_styled_progress_bar_height</item>
|
<item name="bar_height">@dimen/exo_styled_progress_bar_height</item>
|
||||||
|
<item name="bar_gravity">bottom</item>
|
||||||
<item name="touch_target_height">@dimen/exo_styled_progress_touch_target_height</item>
|
<item name="touch_target_height">@dimen/exo_styled_progress_touch_target_height</item>
|
||||||
<item name="scrubber_enabled_size">@dimen/exo_styled_progress_enabled_thumb_size</item>
|
<item name="scrubber_enabled_size">@dimen/exo_styled_progress_enabled_thumb_size</item>
|
||||||
<item name="scrubber_dragged_size">@dimen/exo_styled_progress_dragged_thumb_size</item>
|
<item name="scrubber_dragged_size">@dimen/exo_styled_progress_dragged_thumb_size</item>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/midroll-5s.mp4
|
file:///android_asset/media/mp4/midroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
@ -48,7 +48,7 @@ file:///android_asset/mp4/midroll-5s.mp4
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/midroll-5s.mp4
|
file:///android_asset/media/mp4/midroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/midroll-5s.mp4
|
file:///android_asset/media/mp4/midroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
@ -48,7 +48,7 @@ file:///android_asset/mp4/midroll-5s.mp4
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/midroll-5s.mp4
|
file:///android_asset/media/mp4/midroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/preroll-5s.mp4
|
file:///android_asset/media/mp4/preroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/preroll-5s.mp4
|
file:///android_asset/media/mp4/preroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
@ -48,7 +48,7 @@ file:///android_asset/mp4/preroll-5s.mp4
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/midroll-5s.mp4
|
file:///android_asset/media/mp4/midroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
@ -79,7 +79,7 @@ file:///android_asset/mp4/midroll-5s.mp4
|
|||||||
<MediaFiles>
|
<MediaFiles>
|
||||||
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
file:///android_asset/mp4/postroll-5s.mp4
|
file:///android_asset/media/mp4/postroll-5s.mp4
|
||||||
]]>
|
]]>
|
||||||
</MediaFile>
|
</MediaFile>
|
||||||
</MediaFiles>
|
</MediaFiles>
|
||||||
|
BIN
testdata/src/test/assets/media/tx3g/sample_with_styl_end_too_large
vendored
Normal file
BIN
testdata/src/test/assets/media/tx3g/sample_with_styl_end_too_large
vendored
Normal file
Binary file not shown.
BIN
testdata/src/test/assets/media/tx3g/sample_with_styl_start_too_large
vendored
Normal file
BIN
testdata/src/test/assets/media/tx3g/sample_with_styl_start_too_large
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user