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
### 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) ###
* Core library:
@ -7,6 +68,7 @@
argument ([#8024](https://github.com/google/ExoPlayer/issues/8024)).
* Fix bug where streams with highly uneven track durations may get stuck
in a buffering state
([#7943](https://github.com/google/ExoPlayer/issues/7943)).
* Switch Guava dependency from `implementation` to `api`
([#7905](https://github.com/google/ExoPlayer/issues/7905),
[#7993](https://github.com/google/ExoPlayer/issues/7993)).
@ -54,6 +116,9 @@
([#7992](https://github.com/google/ExoPlayer/issues/7992)).
* FLV: Make files seekable by using the key frame index
([#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
with an I-FRAME only variant
([#8025](https://github.com/google/ExoPlayer/issues/8025)).
@ -63,12 +128,12 @@
* Allow apps to specify a `VideoAdPlayerCallback`
([#7944](https://github.com/google/ExoPlayer/issues/7944)).
* Accept ad tags via the `AdsMediaSource` constructor and deprecate
passing them via the `ImaAdsLoader` constructor/builders. Passing the
ad tag via media item playback properties continues to be supported.
This is in preparation for supporting ads in playlists
passing them via the `ImaAdsLoader` constructor/builders. Passing the ad
tag via media item playback properties continues to be supported. This
is in preparation for supporting ads in playlists
([#3750](https://github.com/google/ExoPlayer/issues/3750)).
* 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
([#8067](https://github.com/google/ExoPlayer/issues/8067)).
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for

View File

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.12.1'
releaseVersionCode = 2012001
releaseVersion = '2.12.2'
releaseVersionCode = 2012002
minSdkVersion = 16
appTargetSdkVersion = 29
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": [
{
"name": "Google Glass H264 (MP4)",
"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",
"extension": "mpd"
"name": "HD (MP4, H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
},
{
"name": "Google Play H264 (MP4)",
"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",
"extension": "mpd"
"name": "UHD (MP4, H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
},
{
"name": "Google Glass VP9 (WebM)",
"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",
"extension": "mpd"
"name": "HD (MP4, H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"
},
{
"name": "Google Play VP9 (WebM)",
"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",
"extension": "mpd"
"name": "UHD (MP4, H265)",
"uri": "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears_uhd.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": [
{
"name": "SW secure crypto (L3)",
@ -102,143 +188,27 @@
]
},
{
"name": "Widevine DASH H264 (MP4)",
"name": "60fps DASH",
"samples": [
{
"name": "Clear",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"
"name": "HD (MP4, H264, Clear)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd"
},
{
"name": "Clear UHD",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears_uhd.mpd"
"name": "4K (MP4, H264, Clear)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd"
},
{
"name": "Secure (cenc)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd",
"name": "HD (MP4, H264, Widevine cenc)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd",
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "Secure UHD (cenc)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears_uhd.mpd",
"name": "4K (MP4, H264, Widevine cenc)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd",
"drm_scheme": "widevine",
"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",
"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"
},
{
"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"
},
{
@ -262,146 +232,26 @@
"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"
},
{
"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"
}
]
},
{
"name": "Misc",
"name": "SmoothStreaming",
"samples": [
{
"name": "Dizzy (MP4)",
"uri": "https://html5demos.com/assets/dizzy.mp4"
"name": "Super speed (MP4, H264, Clear)",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest"
},
{
"name": "Apple 10s (AAC)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
},
{
"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
}
]
"name": "Super speed (MP4, H264, PlayReady)",
"uri": "https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest",
"drm_scheme": "playready"
}
]
},
@ -497,6 +347,105 @@
"name": "VMAP midroll at 1765 s",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
"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",
"samples": [
{
"name": "TTML",
"name": "TTML positioning",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_ttml_sample.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "en"
},
{
"name": "WebVTT line 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": "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]",
"name": "TTML Japanese features",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/japanese-ttml.xml",
"subtitle_mime_type": "application/ttml+xml",
"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",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/japanese.vtt",
"subtitle_mime_type": "text/vtt",
"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": [
{
"name": "Big Buck Bunny (DASH,H264,1080p,Clear)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-1080/manifest.mpd"
"name": "Dizzy (MP4)",
"uri": "https://html5demos.com/assets/dizzy.mp4"
},
{
"name": "Big Buck Bunny (DASH,H264,4K,Clear)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-clear-2160/manifest.mpd"
"name": "Apple 10s (AAC)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac"
},
{
"name": "Big Buck Bunny (DASH,H264,1080p,Widevine)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-1080/manifest.mpd",
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
"name": "Apple 10s (TS)",
"uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts"
},
{
"name": "Big Buck Bunny (DASH,H264,4K,Widevine)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/60fps/bbb-wv-2160/manifest.mpd",
"drm_scheme": "widevine",
"drm_license_uri": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
"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 (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) {
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;
}
@Nullable
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;
}
public void toggleDownload(
FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
if (download != null) {
@Nullable Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
if (download != null && download.state != Download.STATE_FAILED) {
DownloadService.sendRemoveDownload(
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
} else {
@ -223,7 +223,7 @@ public class DownloadTracker {
widevineOfflineLicenseFetchTask =
new WidevineOfflineLicenseFetchTask(
format,
mediaItem.playbackProperties.drmConfiguration.licenseUri,
mediaItem.playbackProperties.drmConfiguration,
httpDataSourceFactory,
/* dialogHelper= */ this,
helper);
@ -373,7 +373,7 @@ public class DownloadTracker {
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
private final Format format;
private final Uri licenseUri;
private final MediaItem.DrmConfiguration drmConfiguration;
private final HttpDataSource.Factory httpDataSourceFactory;
private final StartDownloadDialogHelper dialogHelper;
private final DownloadHelper downloadHelper;
@ -383,12 +383,12 @@ public class DownloadTracker {
public WidevineOfflineLicenseFetchTask(
Format format,
Uri licenseUri,
MediaItem.DrmConfiguration drmConfiguration,
HttpDataSource.Factory httpDataSourceFactory,
StartDownloadDialogHelper dialogHelper,
DownloadHelper downloadHelper) {
this.format = format;
this.licenseUri = licenseUri;
this.drmConfiguration = drmConfiguration;
this.httpDataSourceFactory = httpDataSourceFactory;
this.dialogHelper = dialogHelper;
this.downloadHelper = downloadHelper;
@ -398,8 +398,10 @@ public class DownloadTracker {
protected Void doInBackground(Void... voids) {
OfflineLicenseHelper offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance(
licenseUri.toString(),
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
httpDataSourceFactory,
drmConfiguration.requestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
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.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
@ -66,10 +65,11 @@ import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlaybackPreparer, StyledPlayerControlView.VisibilityListener {
implements OnClickListener, StyledPlayerControlView.VisibilityListener {
// Saved instance state keys.
@ -252,13 +252,6 @@ public class PlayerActivity extends AppCompatActivity
}
}
// PlaybackPreparer implementation
@Override
public void preparePlayback() {
player.prepare();
}
// PlayerControlView.VisibilityListener implementation
@Override
@ -304,7 +297,6 @@ public class PlayerActivity extends AppCompatActivity
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay);
playerView.setPlayer(player);
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
}
@ -335,6 +327,7 @@ public class PlayerActivity extends AppCompatActivity
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
showToast(R.string.error_cleartext_not_permitted);
finish();
return Collections.emptyList();
}
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) {
@ -551,7 +544,9 @@ public class PlayerActivity extends AppCompatActivity
.setCustomCacheKey(downloadRequest.customCacheKey)
.setMimeType(downloadRequest.mimeType)
.setStreamKeys(downloadRequest.streamKeys)
.setDrmKeySetId(downloadRequest.keySetId);
.setDrmKeySetId(downloadRequest.keySetId)
.setDrmLicenseRequestHeaders(getDrmRequestHeaders(item));
mediaItems.add(builder.build());
} else {
mediaItems.add(item);
@ -559,4 +554,10 @@ public class PlayerActivity extends AppCompatActivity
}
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="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>

View File

@ -27,6 +27,10 @@ import com.google.android.gms.cast.MediaTrack;
*/
/* 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
* unknown or not applicable.
@ -39,7 +43,9 @@ import com.google.android.gms.cast.MediaTrack;
return C.TIME_UNSET;
}
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);
try {
boolean connectionOpened = blockUntilConnectTimeout();
if (exception != null) {
throw new OpenException(exception, dataSpec, getStatus(urlRequest));
@Nullable IOException connectionOpenException = exception;
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) {
// The timeout was reached before the connection was opened.
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) {
bytesRemaining = dataSpec.length;
} else {
bytesRemaining = getContentLength(responseInfo);
long contentLength = getContentLength(responseInfo);
bytesRemaining =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
} else {
// 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.
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);
@ -551,7 +552,26 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
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];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
@ -777,7 +797,8 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
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);
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
@ -796,7 +817,8 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
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);
int bytesRead = dataSourceUnderTest.read(returnedBuffer);

View File

@ -25,7 +25,7 @@ android {
}
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 'androidx.annotation:annotation:' + androidxAnnotationVersion
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.trackselection.MappingTrackSelector;
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.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
@ -78,7 +80,7 @@ public final class ImaPlaybackTest {
@Test
public void playbackWithPrerollAdTag_playsAdAndContent() throws Exception {
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};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
@ -90,7 +92,8 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls_playsAdAndContent() throws Exception {
String adsResponse =
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};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
@ -102,7 +105,7 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls1And7_playsAdsAndContent() throws Exception {
String adsResponse =
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};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
@ -114,7 +117,7 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls10And20WithSeekTo12_playsAdsAndContent() throws Exception {
String adsResponse =
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};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
@ -131,7 +134,7 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls10And20WithSeekTo18_playsAdsAndContent() throws Exception {
String adsResponse =
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};
ImaHostedTest hostedTest =
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 final Uri contentUri;
private final String adsResponse;
private final DataSpec adTagDataSpec;
private final List<AdId> expectedAdIds;
private final List<AdId> seenAdIds;
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.
super(ImaPlaybackTest.class.getSimpleName(), /* fullPlaybackNoSeeking= */ false);
this.contentUri = contentUri;
this.adsResponse = adsResponse;
this.adTagDataSpec =
new DataSpec(
Util.getDataUriForString(/* mimeType= */ "text/xml", /* data= */ adsResponse));
this.expectedAdIds = Arrays.asList(expectedAdIds);
seenAdIds = new ArrayList<>();
}
@ -226,7 +231,7 @@ public final class ImaPlaybackTest {
}
});
Context context = host.getApplicationContext();
imaAdsLoader = new ImaAdsLoader.Builder(context).buildForAdsResponse(adsResponse);
imaAdsLoader = new ImaAdsLoader.Builder(context).build();
imaAdsLoader.setPlayer(player);
return player;
}
@ -242,7 +247,8 @@ public final class ImaPlaybackTest {
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
return new AdsMediaSource(
contentMediaSource,
dataSourceFactory,
adTagDataSpec,
new DefaultMediaSourceFactory(dataSourceFactory),
Assertions.checkNotNull(imaAdsLoader),
new AdViewProvider() {

View File

@ -82,6 +82,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -705,7 +706,9 @@ public final class ImaAdsLoader
if (adTagUri != null) {
adTagDataSpec = new DataSpec(adTagUri);
} else if (adsResponse != null) {
adTagDataSpec = new DataSpec(Util.getDataUriForString(adsResponse, "text/xml"));
adTagDataSpec =
new DataSpec(
Util.getDataUriForString(/* mimeType= */ "text/xml", /* data= */ adsResponse));
} else {
throw new IllegalStateException();
}
@ -871,6 +874,7 @@ public final class ImaAdsLoader
if (configuration.applicationAdErrorListener != null) {
adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener);
}
adsLoader.release();
}
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
@ -1118,6 +1122,10 @@ public final class ImaAdsLoader
private void updateAdProgress() {
VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate();
if (configuration.debugModeEnabled) {
Log.d(TAG, "Ad progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
}
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate);
@ -1211,17 +1219,31 @@ public final class ImaAdsLoader
if (imaAdInfo != null) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
updateAdPlaybackState();
} else if (adPlaybackState.adGroupCount == 1 && adPlaybackState.adGroupTimesUs[0] == 0) {
// For incompatible VPAID ads with one preroll, content is resumed immediately. In this case
// we haven't received ad info (the ad never loaded), but there is only one ad group to skip.
adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ 0);
updateAdPlaybackState();
} else {
// Mark any ads for the current/reported player position that haven't loaded as being in the
// error state, to force resuming content. This includes VPAID ads that never load.
long playerPositionUs;
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) {
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
bufferingAd = true;
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onBuffering(adMediaInfo);
@ -1282,13 +1304,18 @@ public final class ImaAdsLoader
if (adMediaInfo == null) {
Log.w(TAG, "onEnded without ad media info");
} else {
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo);
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
if (playingAdIndexInAdGroup == C.INDEX_UNSET
|| (adInfo != null && adInfo.adIndexInAdGroup < playingAdIndexInAdGroup)) {
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo);
}
if (configuration.debugModeEnabled) {
Log.d(
TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
}
if (configuration.debugModeEnabled) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
int adGroupIndex = player.getCurrentAdGroupIndex();
@ -1716,15 +1743,9 @@ public final class ImaAdsLoader
public VideoProgressUpdate getContentProgress() {
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
if (configuration.debugModeEnabled) {
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
Log.d(TAG, "Content progress: not ready");
} else {
Log.d(
TAG,
Util.formatInvariant(
"Content progress: %.1f of %.1f s",
videoProgressUpdate.getCurrentTime(), videoProgressUpdate.getDuration()));
}
Log.d(
TAG,
"Content progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
}
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) {
@ -1893,7 +1914,9 @@ public final class ImaAdsLoader
private static final class DefaultImaFactory implements ImaUtil.ImaFactory {
@Override
public ImaSdkSettings createImaSdkSettings() {
return ImaSdkFactory.getInstance().createImaSdkSettings();
ImaSdkSettings settings = ImaSdkFactory.getInstance().createImaSdkSettings();
settings.setLanguage(getImaLanguageCodeForDefaultLocale());
return settings;
}
@Override
@ -1934,5 +1957,17 @@ public final class ImaAdsLoader
return ImaSdkFactory.getInstance()
.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.UiElement;
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.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
@ -202,5 +203,16 @@ import java.util.Set;
|| 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() {}
}

View File

@ -78,10 +78,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The adapter calls
* {@link ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* 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) {
this.playbackPreparer = playbackPreparer;
}
@ -167,11 +172,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
}
// Calls deprecated method to provide backwards compatibility.
@SuppressWarnings("deprecation")
@Override
public void play() {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);

View File

@ -15,6 +15,7 @@
*/
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_PLAYING;
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.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
@ -762,18 +764,42 @@ public class SessionPlayerConnectorTest {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor, new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch));
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
sessionPlayerConnector.registerPlayerCallback(executor, callback);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
assertThat(onCurrentMediaItemChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS))
.isTrue();
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
assertThat(sessionPlayerConnector.getPlaylist()).isEqualTo(playlist);
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
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
@ -826,7 +852,6 @@ public class SessionPlayerConnectorTest {
}
}
});
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
@ -959,14 +984,12 @@ public class SessionPlayerConnectorTest {
int listSize = 2;
List<MediaItem> playlist = TestUtils.createPlaylist(listSize);
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor, new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch));
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
sessionPlayerConnector.registerPlayerCallback(executor, callback);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(0);
assertThat(onCurrentMediaItemChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS))
.isTrue();
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
}
@Test
@ -1194,16 +1217,15 @@ public class SessionPlayerConnectorTest {
int listSize = playlist.size();
// 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<>();
PlayerCallbackForPlaylist callback =
new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch) {
new PlayerCallbackForPlaylist(playlist, listSize + 2) {
@Override
public void onCurrentMediaItemChanged(
@NonNull SessionPlayer player, @NonNull MediaItem item) {
super.onCurrentMediaItemChanged(player, item);
currentMediaItemChanges.add(item);
onCurrentMediaItemChangedLatch.countDown();
countDown();
}
@Override
@ -1224,7 +1246,7 @@ public class SessionPlayerConnectorTest {
assertWithMessage(
"Current media item didn't change as expected. Actual changes were %s",
currentMediaItemChanges)
.that(onCurrentMediaItemChangedLatch.await(PLAYBACK_COMPLETED_WAIT_TIME_MS, MILLISECONDS))
.that(callback.await(PLAYBACK_COMPLETED_WAIT_TIME_MS, MILLISECONDS))
.isTrue();
int expectedMediaItemIndex = 0;
@ -1286,9 +1308,9 @@ public class SessionPlayerConnectorTest {
private List<MediaItem> playlist;
private CountDownLatch onCurrentMediaItemChangedLatch;
PlayerCallbackForPlaylist(List<MediaItem> playlist, CountDownLatch latch) {
PlayerCallbackForPlaylist(List<MediaItem> playlist, int count) {
this.playlist = playlist;
onCurrentMediaItemChangedLatch = latch;
onCurrentMediaItemChangedLatch = new CountDownLatch(count);
}
@Override
@ -1297,5 +1319,17 @@ public class SessionPlayerConnectorTest {
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(currentIndex);
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. */
void onSeekCompleted();
/** Called when the player rebuffers. */
/** Called when the player starts buffering. */
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(
androidx.media2.common.MediaItem media2MediaItem, int bufferingPercentage);
@ -118,8 +118,9 @@ import java.util.List;
private final List<MediaItem> exoPlayerPlaylist;
private ControlDispatcher controlDispatcher;
private int sessionPlayerState;
private boolean prepared;
private boolean rebuffering;
@Nullable private androidx.media2.common.MediaItem bufferingItem;
private int currentWindowIndex;
private boolean ignoreTimelineUpdates;
@ -149,11 +150,14 @@ import java.util.List;
media2Playlist = new ArrayList<>();
exoPlayerPlaylist = new ArrayList<>();
currentWindowIndex = C.INDEX_UNSET;
prepared = player.getPlaybackState() != Player.STATE_IDLE;
rebuffering = player.getPlaybackState() == Player.STATE_BUFFERING;
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) {
@ -198,6 +202,9 @@ import java.util.List;
}
public boolean removePlaylistItem(@IntRange(from = 0) int index) {
if (player.getMediaItemCount() <= index) {
return false;
}
player.removeMediaItem(index);
return true;
}
@ -353,7 +360,7 @@ import java.util.List;
}
/* @SessionPlayer.PlayerState */
private int getState() {
private int evaluateSessionPlayerState() {
if (hasError()) {
return SessionPlayer.PLAYER_STATE_ERROR;
}
@ -363,7 +370,9 @@ import java.util.List;
case Player.STATE_IDLE:
return SessionPlayer.PLAYER_STATE_IDLE;
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_READY:
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) {
Player.AudioComponent audioComponent = Assertions.checkStateNotNull(player.getAudioComponent());
audioComponent.setAudioAttributes(
@ -397,7 +465,7 @@ import java.util.List;
public void reset() {
controlDispatcher.dispatchStop(player, /* reset= */ true);
prepared = false;
rebuffering = false;
bufferingItem = null;
}
public void close() {
@ -433,35 +501,6 @@ import java.util.List;
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) {
int currentWindowIndex = getCurrentMediaItemIndex();
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
private boolean isExoPlayerMediaItemsChanged(Timeline timeline) {
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() {
androidx.media2.common.MediaItem media2MediaItem =
Assertions.checkNotNull(getCurrentMediaItem());
@ -553,39 +560,6 @@ import java.util.List;
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) {
try {
if (media2MediaItem instanceof CallbackMediaItem) {
@ -602,12 +576,12 @@ import java.util.List;
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
handlePlayWhenReadyChanged();
updateSessionPlayerState();
}
@Override
public void onPlaybackStateChanged(@Player.State int state) {
handlePlayerStateChanged(state);
handlePlayerStateChanged();
}
@Override
@ -617,34 +591,41 @@ import java.util.List;
@Override
public void onPlayerError(ExoPlaybackException error) {
handlePlayerError();
updateSessionPlayerState();
}
@Override
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
handleRepeatModeChanged(repeatMode);
listener.onRepeatModeChanged(Utils.getRepeatMode(repeatMode));
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
handleShuffleMode(shuffleModeEnabled);
listener.onShuffleModeChanged(Utils.getShuffleMode(shuffleModeEnabled));
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
handlePlaybackParametersChanged(playbackParameters);
listener.onPlaybackSpeedChanged(playbackParameters.speed);
}
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
handleTimelineChanged(timeline);
if (ignoreTimelineUpdates) {
return;
}
if (!isExoPlayerMediaItemsChanged(timeline)) {
return;
}
updatePlaylist(timeline);
listener.onPlaylistChanged();
}
// AudioListener implementation.
@Override
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() {
List<MediaItem> currentPlaylist = player.getPlaylist();
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
MediaItem currentMediaItem = player.getCurrentMediaItem();
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
boolean notifyCurrentMediaItem =
!ObjectsCompat.equals(this.currentMediaItem, currentMediaItem) && currentMediaItem != null;
this.currentMediaItem = currentMediaItem;
long currentPosition = getCurrentPosition();
@ -573,9 +577,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
callback.onPlaylistChanged(
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
if (notifyCurrentMediaItem) {
Assertions.checkNotNull(
currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null");
callback.onCurrentMediaItemChanged(SessionPlayerConnector.this, currentMediaItem);
// 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 (playbackPreparer != null) {
playbackPreparer.onPrepare(/* playWhenReady= */ true);
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
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());
responseByteStream = responseBody.byteStream();
} 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(
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}

View File

@ -7,3 +7,12 @@
# From https://github.com/google/guava/wiki/UsingProGuardWithGuava
-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_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_USE_DEFAULT}.
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM} or {@link #STREAM_TYPE_VOICE_CALL}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@ -265,8 +264,7 @@ public final class C {
STREAM_TYPE_NOTIFICATION,
STREAM_TYPE_RING,
STREAM_TYPE_SYSTEM,
STREAM_TYPE_VOICE_CALL,
STREAM_TYPE_USE_DEFAULT
STREAM_TYPE_VOICE_CALL
})
public @interface StreamType {}
/**
@ -297,13 +295,7 @@ public final class C {
* @see AudioManager#STREAM_VOICE_CALL
*/
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
/**
* @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.
*/
/** The default stream type used by audio renderers. Equal to {@link #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". */
// 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}. */
// 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.
@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// 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. */
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.
*
* <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.
*
* <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
* include their own DRM license server URI.
* Sets whether to force use the default DRM license server URI even if the media specifies its
* own DRM license server URI.
*
* <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.
@ -482,8 +482,8 @@ public final class MediaItem {
public final UUID uuid;
/**
* Optional DRM license server {@link Uri}. If {@code null} then the DRM license server must be
* specified by the media.
* Optional default DRM license server {@link Uri}. If {@code null} then the DRM license server
* must be specified by the media.
*/
@Nullable public final Uri licenseUri;
@ -500,8 +500,8 @@ public final class MediaItem {
public final boolean playClearContentWithoutKey;
/**
* Sets whether to use the DRM license server URI of the media item for key requests that
* include their own DRM license server URI.
* Whether to force use of {@link #licenseUri} even if the media specifies its own DRM license
* server URI.
*/
public final boolean forceDefaultLicenseUri;
@ -519,6 +519,7 @@ public final class MediaItem {
boolean playClearContentWithoutKey,
List<Integer> drmSessionForClearTypes,
@Nullable byte[] keySetId) {
Assertions.checkArgument(!(forceDefaultLicenseUri && licenseUri == null));
this.uuid = uuid;
this.licenseUri = licenseUri;
this.requestHeaders = requestHeaders;

View File

@ -271,7 +271,24 @@ public interface HttpDataSource extends DataSource {
this.dataSpec = dataSpec;
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);
this.contentType = contentType;
}
}
/**

View File

@ -239,6 +239,50 @@ public final class MimeTypes {
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.
*

View File

@ -515,8 +515,8 @@ public final class ParsableByteArray {
* Reads a line of text.
*
* <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
* charset (UTF-8) is used. This method discards leading UTF-8 byte order marks, if present.
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The UTF-8 charset is
* 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
* has already been reached.

View File

@ -1485,6 +1485,18 @@ public final class Util {
+ ") " + 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
* trackType}.
@ -1677,7 +1689,6 @@ public final class Util {
return C.USAGE_ASSISTANCE_SONIFICATION;
case C.STREAM_TYPE_VOICE_CALL:
return C.USAGE_VOICE_COMMUNICATION;
case C.STREAM_TYPE_USE_DEFAULT:
case C.STREAM_TYPE_MUSIC:
default:
return C.USAGE_MEDIA;
@ -1698,7 +1709,6 @@ public final class Util {
return C.CONTENT_TYPE_SONIFICATION;
case C.STREAM_TYPE_VOICE_CALL:
return C.CONTENT_TYPE_SPEECH;
case C.STREAM_TYPE_USE_DEFAULT:
case C.STREAM_TYPE_MUSIC:
default:
return C.CONTENT_TYPE_MUSIC;

View File

@ -28,6 +28,69 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
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
public void isText_returnsCorrectResult() {
assertThat(MimeTypes.isText(MimeTypes.TEXT_VTT)).isTrue();

View File

@ -26,6 +26,14 @@ import com.google.android.exoplayer2.Player.RepeatMode;
*/
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.
*

View File

@ -52,6 +52,12 @@ public class DefaultControlDispatcher implements ControlDispatcher {
window = new Timeline.Window();
}
@Override
public boolean dispatchPrepare(Player player) {
player.prepare();
return true;
}
@Override
public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
player.setPlayWhenReady(playWhenReady);

View File

@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer2;
/** Called to prepare a playback. */
/** @deprecated Use {@link ControlDispatcher} instead. */
@Deprecated
public interface PlaybackPreparer {
/** Called to prepare a playback. */
/** @deprecated Use {@link ControlDispatcher#dispatchPrepare(Player)} instead. */
@Deprecated
void preparePlayback();
}

View File

@ -722,15 +722,21 @@ public interface Player {
@IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
@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;
/**
* "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;
/**
* "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;
@ -1126,26 +1132,41 @@ public interface Player {
/**
* Returns whether a previous window exists, which may depend on the current repeat mode and
* 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();
/**
* Seeks to the default position of the previous window in the timeline, which may depend on the
* current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()}
* is {@code false}.
* Seeks to the default position of the previous window, which may depend on the current repeat
* mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()} is {@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 previous();
/**
* Returns whether a next window exists, which may depend on the current repeat mode and 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 hasNext();
/**
* Seeks to the default position of the next window in the timeline, which may depend on the
* current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is
* {@code false}.
* Seeks to the default position of the next window, which may depend on the current repeat mode
* and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is {@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();
@ -1254,18 +1275,24 @@ public interface Player {
int getCurrentWindowIndex();
/**
* Returns the index of the next timeline window to be played, which may depend on the current
* repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window
* currently being played is the last window or if the {@link #getCurrentTimeline() current
* timeline} is empty.
* Returns the index of the window that will be played if {@link #next()} is called, which may
* depend on the current repeat mode and whether shuffle mode is enabled. Returns {@link
* C#INDEX_UNSET} if {@link #hasNext()} is {@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.
*/
int getNextWindowIndex();
/**
* Returns the index of the previous timeline window to be played, which may depend on the current
* repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window
* currently being played is the first window or if the {@link #getCurrentTimeline() current
* timeline} is empty.
* Returns the index of the window that will be played if {@link #previous()} is called, which may
* depend on the current repeat mode and whether shuffle mode is enabled. Returns {@link
* C#INDEX_UNSET} if {@link #hasPrevious()} is {@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.
*/
int getPreviousWindowIndex();

View File

@ -21,7 +21,9 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
/** 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);
}
private static final String TAG = "StreamVolumeManager";
// TODO(b/151280453): Replace the hidden intent action with an official one.
// Copied from AudioManager#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 Listener listener;
private final AudioManager audioManager;
private final VolumeChangeReceiver receiver;
@Nullable private VolumeChangeReceiver receiver;
@C.StreamType private int streamType;
private int volume;
private boolean muted;
private boolean released;
/** Creates a manager. */
public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) {
@ -68,9 +71,14 @@ import com.google.android.exoplayer2.util.Util;
volume = getVolumeFromManager(audioManager, streamType);
muted = getMutedFromManager(audioManager, streamType);
receiver = new VolumeChangeReceiver();
VolumeChangeReceiver receiver = new VolumeChangeReceiver();
IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION);
applicationContext.registerReceiver(receiver, filter);
try {
applicationContext.registerReceiver(receiver, filter);
this.receiver = receiver;
} catch (RuntimeException e) {
Log.w(TAG, "Error registering stream volume receiver", e);
}
}
/** Sets the audio stream type. */
@ -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. */
public void release() {
if (released) {
return;
if (receiver != null) {
try {
applicationContext.unregisterReceiver(receiver);
} catch (RuntimeException e) {
Log.w(TAG, "Error unregistering stream volume receiver", e);
}
receiver = null;
}
applicationContext.unregisterReceiver(receiver);
released = true;
}
private void updateVolumeAndNotifyIfChanged() {
@ -177,7 +188,14 @@ import com.google.android.exoplayer2.util.Util;
}
private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) {
return audioManager.getStreamVolume(streamType);
// AudioManager#getStreamVolume(int) throws an exception on some devices. See
// https://github.com/google/ExoPlayer/issues/8191.
try {
return audioManager.getStreamVolume(streamType);
} catch (RuntimeException e) {
Log.w(TAG, "Could not retrieve stream volume for stream type " + streamType, e);
return audioManager.getStreamMaxVolume(streamType);
}
}
private static boolean getMutedFromManager(
@ -185,7 +203,7 @@ import com.google.android.exoplayer2.util.Util;
if (Util.SDK_INT >= 23) {
return audioManager.isStreamMute(streamType);
} 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);
/**
* Scales the specified playout duration to take into account speedup due to audio processing,
* returning an input media duration, in arbitrary units.
* Returns the media duration corresponding to the specified playout duration, taking speed
* 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);
@ -173,9 +180,9 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
float speed = sonicAudioProcessor.setSpeed(playbackParameters.speed);
float pitch = sonicAudioProcessor.setPitch(playbackParameters.pitch);
return new PlaybackParameters(speed, pitch);
sonicAudioProcessor.setSpeed(playbackParameters.speed);
sonicAudioProcessor.setPitch(playbackParameters.pitch);
return playbackParameters;
}
@Override
@ -186,7 +193,7 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public long getMediaDuration(long playoutDuration) {
return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration);
return sonicAudioProcessor.getMediaDuration(playoutDuration);
}
@Override
@ -1369,21 +1376,33 @@ public final class DefaultAudioSink implements AudioSink {
mediaPositionParameters = mediaPositionParametersCheckpoints.remove();
}
long playoutDurationSinceLastCheckpoint =
long playoutDurationSinceLastCheckpointUs =
positionUs - mediaPositionParameters.audioTrackPositionUs;
if (!mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
if (mediaPositionParametersCheckpoints.isEmpty()) {
playoutDurationSinceLastCheckpoint =
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpoint);
} else {
// Playing data at a previous playback speed, so fall back to multiplying by the speed.
playoutDurationSinceLastCheckpoint =
Util.getMediaDurationForPlayoutDuration(
playoutDurationSinceLastCheckpoint,
mediaPositionParameters.playbackParameters.speed);
}
if (mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpointUs;
} else if (mediaPositionParametersCheckpoints.isEmpty()) {
long mediaDurationSinceLastCheckpointUs =
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpointUs);
return mediaPositionParameters.mediaTimeUs + mediaDurationSinceLastCheckpointUs;
} else {
// The processor chain has been configured with new parameters, but we're still playing audio
// 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(
playoutDurationUntilNextCheckpointUs,
mediaPositionParameters.playbackParameters.speed);
return nextMediaPositionParameters.mediaTimeUs - mediaDurationUntilNextCheckpointUs;
}
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint;
}
private long applySkipping(long positionUs) {

View File

@ -97,6 +97,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private long currentPositionUs;
private boolean allowFirstBufferPositionDiscontinuity;
private boolean allowPositionDiscontinuity;
private boolean audioSinkNeedsReset;
private boolean experimentalKeepAudioTrackOnSeek;
@ -507,6 +508,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void onDisabled() {
audioSinkNeedsReset = true;
try {
audioSink.flush();
} finally {
@ -523,7 +525,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
try {
super.onReset();
} finally {
audioSink.reset();
if (audioSinkNeedsReset) {
audioSinkNeedsReset = false;
audioSink.reset();
}
}
}

View File

@ -83,6 +83,14 @@ import java.util.Arrays;
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
* consumed.

View File

@ -15,10 +15,11 @@
*/
package com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@ -36,10 +37,10 @@ public final class SonicAudioProcessor implements AudioProcessor {
private static final float CLOSE_THRESHOLD = 0.01f;
/**
* The minimum number of output bytes at which the speedup is calculated using the input/output
* byte counts, rather than using the current playback parameters speed.
* The minimum number of output bytes required for duration scaling to be calculated using the
* 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 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
* {@link #flush() flushed} before queueing more data.
*
* @param speed The requested new playback speed.
* @return The actual new playback speed.
* @param speed The target playback speed.
*/
public float setSpeed(float speed) {
public void setSpeed(float speed) {
if (this.speed != speed) {
this.speed = speed;
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
* {@link #flush() flushed} before queueing more data.
*
* @param pitch The requested new pitch.
* @return The actual new pitch.
* @param pitch The target pitch.
*/
public float setPitch(float pitch) {
public void setPitch(float pitch) {
if (this.pitch != pitch) {
this.pitch = pitch;
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,
* in the same units as {@code duration}.
* Returns the media duration corresponding to the specified playout duration, taking speed
* adjustment into account.
*
* @param duration The duration to scale taking into account speedup.
* @return The specified duration scaled to take into account speedup, in the same units as
* {@code duration}.
* <p>The scaling performed by this method will use the actual playback speed achieved by the
* audio processor, 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}.
*/
public long scaleDurationForSpeedup(long duration) {
if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) {
public long getMediaDuration(long playoutDuration) {
if (outputBytes >= MIN_BYTES_FOR_DURATION_SCALING_CALCULATION) {
long processedInputBytes = inputBytes - checkNotNull(sonic).getPendingInputBytes();
return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate
? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes)
? Util.scaleLargeTimestamp(playoutDuration, processedInputBytes, outputBytes)
: Util.scaleLargeTimestamp(
duration,
inputBytes * outputAudioFormat.sampleRate,
playoutDuration,
processedInputBytes * outputAudioFormat.sampleRate,
outputBytes * inputAudioFormat.sampleRate);
} else {
return (long) ((double) speed * duration);
return (long) ((double) speed * playoutDuration);
}
}
@ -164,7 +165,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public void queueInput(ByteBuffer inputBuffer) {
Sonic sonic = Assertions.checkNotNull(this.sonic);
Sonic sonic = checkNotNull(this.sonic);
if (inputBuffer.hasRemaining()) {
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
int inputSize = inputBuffer.remaining();

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
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.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -39,29 +41,35 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
private static final int MAX_MANUAL_REDIRECTS = 5;
private final HttpDataSource.Factory dataSourceFactory;
private final String defaultLicenseUrl;
@Nullable private final String defaultLicenseUrl;
private final boolean forceDefaultLicenseUrl;
private final Map<String, String> keyRequestProperties;
/**
* @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.
*/
public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
public HttpMediaDrmCallback(
@Nullable String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
this(defaultLicenseUrl, /* forceDefaultLicenseUrl= */ false, dataSourceFactory);
}
/**
* @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
* set to true.
* @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
* include their own license URL.
* their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is set to
* true. May be {@code null} if {@code forceDefaultLicenseUrl} is {@code false} and if it's
* known that all key requests will specify their own URLs.
* @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.
*/
public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,
public HttpMediaDrmCallback(
@Nullable String defaultLicenseUrl,
boolean forceDefaultLicenseUrl,
HttpDataSource.Factory dataSourceFactory) {
Assertions.checkArgument(!(forceDefaultLicenseUrl && TextUtils.isEmpty(defaultLicenseUrl)));
this.dataSourceFactory = dataSourceFactory;
this.defaultLicenseUrl = defaultLicenseUrl;
this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
@ -121,6 +129,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
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<>();
// Add standard request properties for supported schemes.
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 DataSpec dataSpec;
private final CacheDataSource dataSource;
private final CacheWriter cacheWriter;
@Nullable private final PriorityTaskManager priorityTaskManager;
@Nullable private ProgressListener progressListener;
@ -101,6 +102,15 @@ public final class ProgressiveDownloader implements Downloader {
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
.build();
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();
}
@ -108,28 +118,19 @@ public final class ProgressiveDownloader implements Downloader {
public void download(@Nullable ProgressListener progressListener)
throws IOException, InterruptedException {
this.progressListener = progressListener;
if (downloadRunnable == null) {
CacheWriter cacheWriter =
new CacheWriter(
dataSource,
dataSpec,
/* allowShortContent= */ false,
/* temporaryBuffer= */ null,
this::onProgress);
downloadRunnable =
new RunnableFutureTask<Void, IOException>() {
@Override
protected Void doWork() throws IOException {
cacheWriter.cache();
return null;
}
downloadRunnable =
new RunnableFutureTask<Void, IOException>() {
@Override
protected Void doWork() throws IOException {
cacheWriter.cache();
return null;
}
@Override
protected void cancelWork() {
cacheWriter.cancel();
}
};
}
@Override
protected void cancelWork() {
cacheWriter.cancel();
}
};
if (priorityTaskManager != null) {
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.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaItem;
@ -68,7 +67,7 @@ public final class MediaSourceDrmHelper {
Assertions.checkNotNull(mediaItem.playbackProperties);
@Nullable
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();
}
HttpDataSource.Factory dataSourceFactory =
@ -77,7 +76,7 @@ public final class MediaSourceDrmHelper {
: new DefaultHttpDataSourceFactory(userAgent != null ? userAgent : DEFAULT_USER_AGENT);
HttpMediaDrmCallback httpDrmCallback =
new HttpMediaDrmCallback(
castNonNull(drmConfiguration.licenseUri).toString(),
drmConfiguration.licenseUri == null ? null : drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory);
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {

View File

@ -435,6 +435,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) {
// Discard as much as we can synchronously.
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
loader.cancelLoading();
} else {
loader.clearFatalError();

View File

@ -894,6 +894,11 @@ public class SampleQueue implements TrackOutput {
if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
// We've found a suitable sample.
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++;
if (searchIndex == capacity) {

View File

@ -315,6 +315,11 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
mediaChunks.clear();
nextNotifyPrimaryFormatMediaChunkIndex = 0;
if (loader.isLoading()) {
// Discard as much as we can synchronously.
primarySampleQueue.discardToEnd();
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.discardToEnd();
}
loader.cancelLoading();
} else {
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.Subtitle;
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.Util;
import com.google.common.base.Charsets;
@ -43,6 +44,8 @@ import java.util.List;
*/
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_LE = '\uFFFE';
@ -185,6 +188,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
int fontFace = parsableByteArray.readUnsignedByte();
parsableByteArray.skipBytes(1); // font size
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);
attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);
}

View File

@ -306,6 +306,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
try {
connection = makeConnection(dataSpec);
} 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(
"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.ParserException;
import com.google.android.exoplayer2.upstream.HttpDataSource.CleartextNotPermittedException;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException;
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
* FileNotFoundException} or {@link UnexpectedLoaderException}. The retry delay is calculated as
* {@code Math.min((errorCount - 1) * 1000, 5000)}.
* FileNotFoundException}, {@link CleartextNotPermittedException} or {@link
* UnexpectedLoaderException}. The retry delay is calculated as {@code Math.min((errorCount - 1) *
* 1000, 5000)}.
*/
@Override
public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
IOException exception = loadErrorInfo.exception;
return exception instanceof ParserException
|| exception instanceof FileNotFoundException
|| exception instanceof CleartextNotPermittedException
|| exception instanceof UnexpectedLoaderException
? C.TIME_UNSET
: 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);
}
@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
public void discardToDontStopAtReadPosition() {
writeTestData();

View File

@ -15,24 +15,18 @@
*/
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 org.junit.Assert.fail;
import android.graphics.Color;
import android.graphics.Typeface;
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.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import java.io.IOException;
import com.google.common.collect.ImmutableList;
import java.util.Collections;
import org.junit.Test;
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 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_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 =
"media/tx3g/sample_with_styl_all_defaults";
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";
@Test
public void decodeNoSubtitle() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeNoSubtitle() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), NO_SUBTITLE);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertThat(subtitle.getCues(0)).isEmpty();
}
@Test
public void decodeJustText() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeJustText() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_JUST_TEXT);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
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);
}
@Test
public void decodeWithStyl() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeWithStyl() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("CC Test");
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(3);
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
findSpan(text, 0, 6, UnderlineSpan.class);
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
assertThat(text).hasBoldItalicSpanBetween(0, 6);
assertThat(text).hasUnderlineSpanBetween(0, 6);
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
}
/**
* 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);
}
@Test
public void decodeWithStylAllDefaults() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeWithStylAllDefaults() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL_ALL_DEFAULTS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
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);
}
@Test
public void decodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeUtf16BeNoStyl() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_BE_NO_STYL);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
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);
}
@Test
public void decodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeUtf16LeNoStyl() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_UTF16_LE_NO_STYL);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
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);
}
@Test
public void decodeWithMultipleStyl() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeWithMultipleStyl() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_MULTIPLE_STYL);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("Line 2\nLine 3");
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(4);
StyleSpan styleSpan = findSpan(text, 0, 5, StyleSpan.class);
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.ITALIC);
findSpan(text, 7, 12, UnderlineSpan.class);
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);
assertThat(text).hasItalicSpanBetween(0, 5);
assertThat(text).hasUnderlineSpanBetween(7, 12);
assertThat(text).hasForegroundColorSpanBetween(0, 5).withColor(Color.GREEN);
assertThat(text).hasForegroundColorSpanBetween(7, 12).withColor(Color.GREEN);
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
}
@Test
public void decodeWithOtherExtension() throws IOException, SubtitleDecoderException {
Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList());
public void decodeWithOtherExtension() throws Exception {
Tx3gDecoder decoder = new Tx3gDecoder(ImmutableList.of());
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), SAMPLE_WITH_OTHER_EXTENSION);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("CC Test");
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(2);
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD);
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
assertThat(text).hasBoldSpanBetween(0, 6);
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f);
}
@Test
public void initializationDecodeWithStyl() throws IOException, SubtitleDecoderException {
public void initializationDecodeWithStyl() throws Exception {
byte[] initBytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("CC Test");
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(5);
StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
findSpan(text, 0, text.length(), UnderlineSpan.class);
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);
colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
assertThat(text).hasBoldItalicSpanBetween(0, 7);
assertThat(text).hasUnderlineSpanBetween(0, 7);
assertThat(text).hasTypefaceSpanBetween(0, 7).withFamily(C.SERIF_NAME);
// TODO(internal b/171984212): Fix Tx3gDecoder to avoid overlapping spans of the same type.
assertThat(text).hasForegroundColorSpanBetween(0, 7).withColor(Color.RED);
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1f);
}
@Test
public void initializationDecodeWithTbox() throws IOException, SubtitleDecoderException {
public void initializationDecodeWithTbox() throws Exception {
byte[] initBytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), INITIALIZATION);
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_TBOX);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("CC Test");
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(4);
StyleSpan styleSpan = findSpan(text, 0, text.length(), StyleSpan.class);
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
findSpan(text, 0, text.length(), UnderlineSpan.class);
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);
assertThat(text).hasBoldItalicSpanBetween(0, 7);
assertThat(text).hasUnderlineSpanBetween(0, 7);
assertThat(text).hasTypefaceSpanBetween(0, 7).withFamily(C.SERIF_NAME);
assertThat(text).hasForegroundColorSpanBetween(0, 7).withColor(Color.RED);
assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1875f);
}
@Test
public void initializationAllDefaultsDecodeWithStyl()
throws IOException, SubtitleDecoderException {
public void initializationAllDefaultsDecodeWithStyl() throws Exception {
byte[] initBytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), INITIALIZATION_ALL_DEFAULTS);
Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes));
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), SAMPLE_WITH_STYL);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text);
assertThat(text.toString()).isEqualTo("CC Test");
assertThat(text.getSpans(0, text.length(), Object.class)).hasLength(3);
StyleSpan styleSpan = findSpan(text, 0, 6, StyleSpan.class);
assertThat(styleSpan.getStyle()).isEqualTo(Typeface.BOLD_ITALIC);
findSpan(text, 0, 6, UnderlineSpan.class);
ForegroundColorSpan colorSpan = findSpan(text, 0, 6, ForegroundColorSpan.class);
assertThat(colorSpan.getForegroundColor()).isEqualTo(Color.GREEN);
assertThat(text).hasBoldItalicSpanBetween(0, 6);
assertThat(text).hasUnderlineSpanBetween(0, 6);
assertThat(text).hasForegroundColorSpanBetween(0, 6).withColor(Color.GREEN);
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) {
assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION);
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_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_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_ASS = "S_TEXT/ASS";
private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
@ -1743,36 +1745,43 @@ public class MatroskaExtractor implements Extractor {
}
private static boolean isCodecSupported(String codecId) {
return CODEC_ID_VP8.equals(codecId)
|| CODEC_ID_VP9.equals(codecId)
|| CODEC_ID_AV1.equals(codecId)
|| CODEC_ID_MPEG2.equals(codecId)
|| CODEC_ID_MPEG4_SP.equals(codecId)
|| CODEC_ID_MPEG4_ASP.equals(codecId)
|| CODEC_ID_MPEG4_AP.equals(codecId)
|| CODEC_ID_H264.equals(codecId)
|| CODEC_ID_H265.equals(codecId)
|| CODEC_ID_FOURCC.equals(codecId)
|| CODEC_ID_THEORA.equals(codecId)
|| CODEC_ID_OPUS.equals(codecId)
|| CODEC_ID_VORBIS.equals(codecId)
|| CODEC_ID_AAC.equals(codecId)
|| CODEC_ID_MP2.equals(codecId)
|| CODEC_ID_MP3.equals(codecId)
|| CODEC_ID_AC3.equals(codecId)
|| CODEC_ID_E_AC3.equals(codecId)
|| CODEC_ID_TRUEHD.equals(codecId)
|| CODEC_ID_DTS.equals(codecId)
|| CODEC_ID_DTS_EXPRESS.equals(codecId)
|| CODEC_ID_DTS_LOSSLESS.equals(codecId)
|| CODEC_ID_FLAC.equals(codecId)
|| CODEC_ID_ACM.equals(codecId)
|| CODEC_ID_PCM_INT_LIT.equals(codecId)
|| CODEC_ID_SUBRIP.equals(codecId)
|| CODEC_ID_ASS.equals(codecId)
|| CODEC_ID_VOBSUB.equals(codecId)
|| CODEC_ID_PGS.equals(codecId)
|| CODEC_ID_DVBSUB.equals(codecId);
switch (codecId) {
case CODEC_ID_VP8:
case CODEC_ID_VP9:
case CODEC_ID_AV1:
case CODEC_ID_MPEG2:
case CODEC_ID_MPEG4_SP:
case CODEC_ID_MPEG4_ASP:
case CODEC_ID_MPEG4_AP:
case CODEC_ID_H264:
case CODEC_ID_H265:
case CODEC_ID_FOURCC:
case CODEC_ID_THEORA:
case CODEC_ID_OPUS:
case CODEC_ID_VORBIS:
case CODEC_ID_AAC:
case CODEC_ID_MP2:
case CODEC_ID_MP3:
case CODEC_ID_AC3:
case CODEC_ID_E_AC3:
case CODEC_ID_TRUEHD:
case CODEC_ID_DTS:
case CODEC_ID_DTS_EXPRESS:
case CODEC_ID_DTS_LOSSLESS:
case CODEC_ID_FLAC:
case CODEC_ID_ACM:
case CODEC_ID_PCM_INT_LIT:
case CODEC_ID_PCM_INT_BIG:
case CODEC_ID_PCM_FLOAT:
case CODEC_ID_SUBRIP:
case CODEC_ID_ASS:
case CODEC_ID_VOBSUB:
case CODEC_ID_PGS:
case CODEC_ID_DVBSUB:
return true;
default:
return false;
}
}
/**
@ -2102,8 +2111,44 @@ public class MatroskaExtractor implements Extractor {
if (pcmEncoding == C.ENCODING_INVALID) {
pcmEncoding = Format.NO_VALUE;
mimeType = MimeTypes.AUDIO_UNKNOWN;
Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ mimeType);
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);
}
break;
case CODEC_ID_SUBRIP:

View File

@ -277,6 +277,9 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_TTML = 0x54544d4c;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_m1v_ = 0x6d317620;
@SuppressWarnings("ConstantCaseForConstants")
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
|| childAtomType == Atom.TYPE_avc3
|| childAtomType == Atom.TYPE_encv
|| childAtomType == Atom.TYPE_m1v_
|| childAtomType == Atom.TYPE_mp4v
|| childAtomType == Atom.TYPE_hvc1
|| childAtomType == Atom.TYPE_hev1
@ -993,8 +994,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// drmInitData = null;
// }
@Nullable List<byte[]> initializationData = null;
@Nullable String mimeType = null;
if (atomType == Atom.TYPE_m1v_) {
mimeType = MimeTypes.VIDEO_MPEG;
}
@Nullable List<byte[]> initializationData = null;
@Nullable String codecs = null;
@Nullable byte[] projectionData = null;
@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
// exist. If we know from the codec attribute that they don't exist, then we can
// 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;
}
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
if (!MimeTypes.containsCodecsCorrespondingToMimeType(codecs, MimeTypes.VIDEO_H264)) {
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
}
}

View File

@ -603,6 +603,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
}
}
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 =
buildSampleStreamWrapper(
C.TRACK_TYPE_DEFAULT,
@ -614,18 +620,16 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
positionUs);
sampleStreamWrappers.add(sampleStreamWrapper);
manifestUrlIndicesPerWrapper.add(selectedVariantIndices);
if (allowChunklessPreparation && codecs != null) {
boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;
boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null;
if (allowChunklessPreparation && codecsStringAllowsChunklessPreparation) {
List<TrackGroup> muxedTrackGroups = new ArrayList<>();
if (variantsContainVideoCodecs) {
if (numberOfVideoCodecs > 0) {
Format[] videoFormats = new Format[selectedVariantsCount];
for (int i = 0; i < videoFormats.length; i++) {
videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]);
}
muxedTrackGroups.add(new TrackGroup(videoFormats));
if (variantsContainAudioCodecs
if (numberOfAudioCodecs > 0
&& (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) {
muxedTrackGroups.add(
new TrackGroup(
@ -640,7 +644,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
muxedTrackGroups.add(new TrackGroup(ccFormats.get(i)));
}
}
} else if (variantsContainAudioCodecs) {
} else /* numberOfAudioCodecs > 0 */ {
// Variants only contain audio.
Format[] audioFormats = new Format[selectedVariantsCount];
for (int i = 0; i < audioFormats.length; i++) {
@ -651,9 +655,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
/* isPrimaryTrackInVariant= */ true);
}
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 =
@ -693,7 +694,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
continue;
}
boolean renditionsHaveCodecs = true;
boolean codecStringsAllowChunklessPreparation = true;
scratchPlaylistUrls.clear();
scratchPlaylistFormats.clear();
scratchIndicesList.clear();
@ -704,7 +705,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
scratchIndicesList.add(renditionIndex);
scratchPlaylistUrls.add(rendition.url);
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));
sampleStreamWrappers.add(sampleStreamWrapper);
if (allowChunklessPreparation && renditionsHaveCodecs) {
if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) {
Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]);
sampleStreamWrapper.prepareWithMasterPlaylistInfo(
new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0);

View File

@ -490,6 +490,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
loadingFinished = false;
mediaChunks.clear();
if (loader.isLoading()) {
if (sampleQueuesBuilt) {
// Discard as much as we can synchronously.
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
}
loader.cancelLoading();
} else {
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
* 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 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);
@Nullable String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);
@Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs);
@Nullable String sampleMimeType;
@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 =
sampleFormat

View File

@ -152,6 +152,15 @@ public class DefaultTimeBar extends View implements TimeBar {
/** Default color for played ad markers. */
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. */
private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50;
/** 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;
private final int barHeight;
private final int touchTargetHeight;
private final int barGravity;
private final int adMarkerWidth;
private final int scrubberEnabledSize;
private final int scrubberDisabledSize;
@ -286,6 +296,7 @@ public class DefaultTimeBar extends View implements TimeBar {
defaultBarHeight);
touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height,
defaultTouchTargetHeight);
barGravity = a.getInt(R.styleable.DefaultTimeBar_bar_gravity, BAR_GRAVITY_CENTER);
adMarkerWidth = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_ad_marker_width,
defaultAdMarkerWidth);
scrubberEnabledSize = a.getDimensionPixelSize(
@ -318,6 +329,7 @@ public class DefaultTimeBar extends View implements TimeBar {
} else {
barHeight = defaultBarHeight;
touchTargetHeight = defaultTouchTargetHeight;
barGravity = BAR_GRAVITY_CENTER;
adMarkerWidth = defaultAdMarkerWidth;
scrubberEnabledSize = defaultScrubberEnabledSize;
scrubberDisabledSize = defaultScrubberDisabledSize;
@ -659,7 +671,14 @@ public class DefaultTimeBar extends View implements TimeBar {
int barY = (height - touchTargetHeight) / 2;
int seekLeft = getPaddingLeft();
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);
progressBar.set(seekBounds.left + scrubberPadding, progressY,
seekBounds.right - scrubberPadding, progressY + barHeight);

View File

@ -611,11 +611,15 @@ public class PlayerControlView extends FrameLayout {
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* 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) {
this.playbackPreparer = playbackPreparer;
}
@ -1254,11 +1258,14 @@ public class PlayerControlView extends FrameLayout {
}
}
@SuppressWarnings("deprecation")
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (state == Player.STATE_ENDED) {
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
* 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
* when the notification is dismissed by the user.
@ -67,43 +67,55 @@ import java.util.Map;
*
* <h3>Action customization</h3>
*
* Playback actions can be displayed or omitted as follows:
* Playback actions can be included or omitted as follows:
*
* <ul>
* <li><b>{@code useNavigationActions}</b> - Sets whether the previous and next actions are
* 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.
* <li><b>{@code usePlayPauseActions}</b> - Sets whether the play and pause actions are used.
* <ul>
* <li>Corresponding setter: {@link #setUsePlayPauseActions(boolean)}
* <li>Default: {@code true}
* </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
* action is not displayed.
* action is not used.
* <ul>
* <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)}
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS} (5000)
* </ul>
* <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>
* <li>Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)}
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS} (15000)
* </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>
*
* <h3>Overriding drawables</h3>
@ -382,8 +394,10 @@ public class PlayerNotificationManager {
private int currentNotificationTag;
@Nullable private NotificationListener notificationListener;
@Nullable private MediaSessionCompat.Token mediaSessionToken;
private boolean useNavigationActions;
private boolean useNavigationActionsInCompactView;
private boolean usePreviousAction;
private boolean useNextAction;
private boolean usePreviousActionInCompactView;
private boolean useNextActionInCompactView;
private boolean usePlayPauseActions;
private boolean useStopAction;
private int badgeIconType;
@ -610,15 +624,18 @@ public class PlayerNotificationManager {
controlDispatcher = new DefaultControlDispatcher();
window = new Timeline.Window();
instanceId = instanceIdCounter++;
//noinspection Convert2MethodRef
mainHandler =
Util.createHandler(
Looper.getMainLooper(), msg -> PlayerNotificationManager.this.handleMessage(msg));
// This fails the nullness checker because handleMessage() is 'called' while `this` is still
// @UnderInitialization. No tasks are scheduled on mainHandler before the constructor completes,
// so this is safe and we can suppress the warning.
@SuppressWarnings("nullness:methodref.receiver.bound.invalid")
Handler mainHandler = Util.createHandler(Looper.getMainLooper(), this::handleMessage);
this.mainHandler = mainHandler;
notificationManager = NotificationManagerCompat.from(context);
playerListener = new PlayerListener();
notificationBroadcastReceiver = new NotificationBroadcastReceiver();
intentFilter = new IntentFilter();
useNavigationActions = true;
usePreviousAction = true;
useNextAction = true;
usePlayPauseActions = true;
colorized = true;
useChronometer = true;
@ -680,10 +697,16 @@ public class PlayerNotificationManager {
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The manager calls
* {@link ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* 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) {
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) {
if (this.useNavigationActions != useNavigationActions) {
this.useNavigationActions = useNavigationActions;
public void setUseNextAction(boolean useNextAction) {
if (this.useNextAction != useNextAction) {
this.useNextAction = useNextAction;
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
* neither in compact nor in full view mode of the notification.
*
* @param useNavigationActionsInCompactView Whether the navigation actions should be displayed in
* compact view.
* @param usePreviousAction Whether to use the previous action.
*/
public final void setUseNavigationActionsInCompactView(
boolean useNavigationActionsInCompactView) {
if (this.useNavigationActionsInCompactView != useNavigationActionsInCompactView) {
this.useNavigationActionsInCompactView = useNavigationActionsInCompactView;
public void setUsePreviousAction(boolean usePreviousAction) {
if (this.usePreviousAction != usePreviousAction) {
this.usePreviousAction = usePreviousAction;
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.
*
@ -1037,8 +1111,7 @@ public class PlayerNotificationManager {
@Nullable NotificationCompat.Builder builder,
boolean ongoing,
@Nullable Bitmap largeIcon) {
if (player.getPlaybackState() == Player.STATE_IDLE
&& (player.getCurrentTimeline().isEmpty() || playbackPreparer == null)) {
if (player.getPlaybackState() == Player.STATE_IDLE && player.getCurrentTimeline().isEmpty()) {
builderActions = null;
return null;
}
@ -1153,7 +1226,7 @@ public class PlayerNotificationManager {
}
List<String> stringActions = new ArrayList<>();
if (useNavigationActions && enablePrevious) {
if (usePreviousAction && enablePrevious) {
stringActions.add(ACTION_PREVIOUS);
}
if (enableRewind) {
@ -1169,7 +1242,7 @@ public class PlayerNotificationManager {
if (enableFastForward) {
stringActions.add(ACTION_FAST_FORWARD);
}
if (useNavigationActions && enableNext) {
if (useNextAction && enableNext) {
stringActions.add(ACTION_NEXT);
}
if (customActionReceiver != null) {
@ -1194,15 +1267,14 @@ public class PlayerNotificationManager {
protected int[] getActionIndicesForCompactView(List<String> actionNames, Player player) {
int pauseActionIndex = actionNames.indexOf(ACTION_PAUSE);
int playActionIndex = actionNames.indexOf(ACTION_PLAY);
int skipPreviousActionIndex =
useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_PREVIOUS) : -1;
int skipNextActionIndex =
useNavigationActionsInCompactView ? actionNames.indexOf(ACTION_NEXT) : -1;
int previousActionIndex =
usePreviousActionInCompactView ? actionNames.indexOf(ACTION_PREVIOUS) : -1;
int nextActionIndex = useNextActionInCompactView ? actionNames.indexOf(ACTION_NEXT) : -1;
int[] actionIndices = new int[3];
int actionCounter = 0;
if (skipPreviousActionIndex != -1) {
actionIndices[actionCounter++] = skipPreviousActionIndex;
if (previousActionIndex != -1) {
actionIndices[actionCounter++] = previousActionIndex;
}
boolean shouldShowPauseButton = shouldShowPauseButton(player);
if (pauseActionIndex != -1 && shouldShowPauseButton) {
@ -1210,8 +1282,8 @@ public class PlayerNotificationManager {
} else if (playActionIndex != -1 && !shouldShowPauseButton) {
actionIndices[actionCounter++] = playActionIndex;
}
if (skipNextActionIndex != -1) {
actionIndices[actionCounter++] = skipNextActionIndex;
if (nextActionIndex != -1) {
actionIndices[actionCounter++] = nextActionIndex;
}
return Arrays.copyOf(actionIndices, actionCounter);
}
@ -1367,6 +1439,7 @@ public class PlayerNotificationManager {
private class NotificationBroadcastReceiver extends BroadcastReceiver {
@SuppressWarnings("deprecation")
@Override
public void onReceive(Context context, Intent intent) {
Player player = PlayerNotificationManager.this.player;
@ -1380,6 +1453,8 @@ public class PlayerNotificationManager {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
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}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* 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) {
Assertions.checkStateNotNull(controller);
controller.setPlaybackPreparer(playbackPreparer);

View File

@ -834,11 +834,15 @@ public class StyledPlayerControlView extends FrameLayout {
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* 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) {
this.playbackPreparer = playbackPreparer;
}
@ -1698,11 +1702,14 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
@SuppressWarnings("deprecation")
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (state == Player.STATE_ENDED) {
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 subTextView;
private final ImageView iconView;
@ -1930,8 +1937,7 @@ public class StyledPlayerControlView extends FrameLayout {
mainTextView = itemView.findViewById(R.id.exo_main_text);
subTextView = itemView.findViewById(R.id.exo_sub_text);
iconView = itemView.findViewById(R.id.exo_icon);
itemView.setOnClickListener(
v -> onSettingViewClicked(SettingViewHolder.this.getAdapterPosition()));
itemView.setOnClickListener(v -> onSettingViewClicked(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 View checkView;
@ -1977,8 +1983,7 @@ public class StyledPlayerControlView extends FrameLayout {
super(itemView);
textView = itemView.findViewById(R.id.exo_text);
checkView = itemView.findViewById(R.id.exo_check);
itemView.setOnClickListener(
v -> onSubSettingViewClicked(SubSettingViewHolder.this.getAdapterPosition()));
itemView.setOnClickListener(v -> onSubSettingViewClicked(getAdapterPosition()));
}
}

View File

@ -45,6 +45,7 @@ import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
@ -978,11 +979,15 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* 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) {
Assertions.checkStateNotNull(controller);
controller.setPlaybackPreparer(playbackPreparer);

View File

@ -289,10 +289,10 @@ public class TrackSelectionView extends LinearLayout {
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format));
trackView.setTag(trackInfos[trackIndex]);
if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) {
trackView.setFocusable(true);
trackView.setTag(trackInfos[trackIndex]);
trackView.setOnClickListener(componentListener);
} else {
trackView.setFocusable(false);

View File

@ -54,8 +54,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private @MonotonicNonNull SurfaceTexture surfaceTexture;
// Used by other threads only
private volatile @C.StreamType int defaultStereoMode;
private @C.StreamType int lastStereoMode;
@C.StereoMode private volatile int defaultStereoMode;
@C.StereoMode private int lastStereoMode;
@Nullable private byte[] lastProjectionData;
// Methods called on any thread.

View File

@ -19,6 +19,7 @@
android:minWidth="@dimen/exo_setting_width"
android:minHeight="@dimen/exo_settings_height"
android:background="?android:attr/selectableItemBackground"
android:layoutDirection="locale"
android:orientation="horizontal">
<ImageView
@ -38,6 +39,7 @@
android:paddingEnd="4dp"
android:paddingRight="4dp"
android:gravity="center|start"
android:layoutDirection="locale"
android:orientation="vertical">
<TextView
@ -45,6 +47,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/exo_white"
android:textDirection="locale"
android:textSize="@dimen/exo_settings_main_text_size"/>
<TextView
@ -52,6 +55,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/exo_white_opacity_70"
android:textDirection="locale"
android:textSize="@dimen/exo_settings_sub_text_size"/>
</LinearLayout>
</LinearLayout>

View File

@ -19,6 +19,7 @@
android:minWidth="@dimen/exo_setting_width"
android:minHeight="@dimen/exo_settings_height"
android:background="?android:attr/selectableItemBackground"
android:layoutDirection="locale"
android:orientation="horizontal">
<ImageView
@ -40,5 +41,6 @@
android:layout_marginRight="4dp"
android:gravity="center|start"
android:textColor="@color/exo_white"
android:textDirection="locale"
android:textSize="@dimen/exo_settings_main_text_size"/>
</LinearLayout>

View File

@ -72,8 +72,16 @@
<attr name="controller_layout_id" format="reference"/>
<attr name="animation_enabled" format="boolean"/>
<!-- Needed for https://github.com/google/ExoPlayer/issues/7898 -->
<attr name="backgroundTint" format="color"/>
<!-- DefaultTimeBar attributes -->
<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="ad_marker_width" format="dimension"/>
<attr name="scrubber_enabled_size" format="dimension"/>
@ -154,6 +162,7 @@
<attr name="animation_enabled"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
@ -186,6 +195,7 @@
<attr name="controller_layout_id"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
@ -217,6 +227,7 @@
<attr name="animation_enabled"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
@ -233,6 +244,7 @@
<declare-styleable name="DefaultTimeBar">
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<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_enabled_thumb_size">10dp</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_touch_target_height">14dp</dimen>
<dimen name="exo_styled_progress_layout_height">48dp</dimen>
<dimen name="exo_styled_progress_touch_target_height">48dp</dimen>
<dimen name="exo_styled_progress_margin_bottom">52dp</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:paddingBottom">@dimen/exo_icon_padding_bottom</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 name="ExoStyledControls.Button.Center.RewWithAmount">
<item name="android:background">@drawable/exo_ripple_rew</item>
<item name="android:gravity">center|bottom</item>
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</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 name="ExoStyledControls.ButtonText">
@ -187,6 +194,7 @@
<style name="ExoStyledControls.TimeBar">
<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="scrubber_enabled_size">@dimen/exo_styled_progress_enabled_thumb_size</item>
<item name="scrubber_dragged_size">@dimen/exo_styled_progress_dragged_thumb_size</item>

View File

@ -17,7 +17,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
@ -48,7 +48,7 @@ file:///android_asset/mp4/midroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>

View File

@ -17,7 +17,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
@ -48,7 +48,7 @@ file:///android_asset/mp4/midroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>

View File

@ -14,7 +14,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/preroll-5s.mp4
file:///android_asset/media/mp4/preroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>

View File

@ -17,7 +17,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/preroll-5s.mp4
file:///android_asset/media/mp4/preroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
@ -48,7 +48,7 @@ file:///android_asset/mp4/preroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
@ -79,7 +79,7 @@ file:///android_asset/mp4/midroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/postroll-5s.mp4
file:///android_asset/media/mp4/postroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>

Binary file not shown.