Merge pull request #8300 from google/dev-v2-r2.12.2

r2.12.2
This commit is contained in:
Ian Baker 2020-12-01 15:39:11 +00:00 committed by GitHub
commit ffea2a64b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1728 additions and 875 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,12 +1304,17 @@ 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 {
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
if (playingAdIndexInAdGroup == C.INDEX_UNSET
|| (adInfo != null && adInfo.adIndexInAdGroup < playingAdIndexInAdGroup)) {
for (int i = 0; i < adCallbacks.size(); i++) { for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo); adCallbacks.get(i).onEnded(adMediaInfo);
} }
}
if (configuration.debugModeEnabled) { if (configuration.debugModeEnabled) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); 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) {
@ -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(TAG, "Content progress: not ready");
} else {
Log.d( Log.d(
TAG, TAG,
Util.formatInvariant( "Content progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
"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];
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
try {
applicationContext.registerReceiver(receiver, filter); 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); applicationContext.unregisterReceiver(receiver);
released = true; } catch (RuntimeException e) {
Log.w(TAG, "Error unregistering stream volume receiver", e);
}
receiver = null;
}
} }
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) {
// AudioManager#getStreamVolume(int) throws an exception on some devices. See
// https://github.com/google/ExoPlayer/issues/8191.
try {
return audioManager.getStreamVolume(streamType); 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;
} }
} }

View File

@ -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,22 +1376,34 @@ 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 =
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpointUs);
return mediaPositionParameters.mediaTimeUs + mediaDurationSinceLastCheckpointUs;
} else { } else {
// Playing data at a previous playback speed, so fall back to multiplying by the speed. // 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
// 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( Util.getMediaDurationForPlayoutDuration(
playoutDurationSinceLastCheckpoint, playoutDurationUntilNextCheckpointUs,
mediaPositionParameters.playbackParameters.speed); mediaPositionParameters.playbackParameters.speed);
return nextMediaPositionParameters.mediaTimeUs - mediaDurationUntilNextCheckpointUs;
} }
} }
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint;
}
private long applySkipping(long positionUs) { private long applySkipping(long positionUs) {
return positionUs return positionUs

View File

@ -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,9 +525,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
try { try {
super.onReset(); super.onReset();
} finally { } finally {
if (audioSinkNeedsReset) {
audioSinkNeedsReset = false;
audioSink.reset(); audioSink.reset();
} }
} }
}
@Override @Override
public boolean isEnded() { public boolean isEnded() {

View File

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

View File

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

View File

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

View File

@ -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,14 +118,6 @@ 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) {
CacheWriter cacheWriter =
new CacheWriter(
dataSource,
dataSpec,
/* allowShortContent= */ false,
/* temporaryBuffer= */ null,
this::onProgress);
downloadRunnable = downloadRunnable =
new RunnableFutureTask<Void, IOException>() { new RunnableFutureTask<Void, IOException>() {
@Override @Override
@ -129,7 +131,6 @@ public final class ProgressiveDownloader implements Downloader {
cacheWriter.cancel(); cacheWriter.cancel();
} }
}; };
}
if (priorityTaskManager != null) { if (priorityTaskManager != null) {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD); priorityTaskManager.add(C.PRIORITY_DOWNLOAD);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +2111,43 @@ 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(
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); + mimeType);
} }
break; break;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.