diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5a5aa12655..4db085b9e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 diff --git a/constants.gradle b/constants.gradle index 44a61d6baa..b06636dd20 100644 --- a/constants.gradle +++ b/constants.gradle @@ -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. diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 24213918f5..baa89f11ff 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -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" } ] } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index 07f4dd2f6e..80c64bc9bd 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -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 { 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); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index c35080c47f..e1a698acdc 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -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 getDrmRequestHeaders(MediaItem item) { + MediaItem.DrmConfiguration drmConfiguration = item.playbackProperties.drmConfiguration; + return drmConfiguration != null ? drmConfiguration.requestHeaders : null; + } } diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index bd5cd63467..9fe882b162 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -21,7 +21,7 @@ Unexpected intent action: %1$s - Cleartext traffic not permitted + Cleartext HTTP traffic not permitted. See https://exoplayer.dev/issues/cleartext-not-permitted Playback failed diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java index 182afb0468..4f3f52a5f9 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java @@ -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; } /** diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 26a60d3332..b215b6d763 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -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 diff --git a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index ac19c8548d..b0b80a8e12 100644 --- a/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -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); diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index ed20dedb10..8cdfb0dffc 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -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' diff --git a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java index 88bc4e14c5..9527d35cef 100644 --- a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java +++ b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java @@ -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 expectedAdIds; private final List 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() { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 4185a158f7..252aa2e396 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -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]; + } } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java index a4f1ec92cc..6d69547278 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java @@ -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() {} } diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 6538160b8b..6da02bb324 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -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); diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java index b80cbe5a5f..1747b3aed7 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java @@ -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 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 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 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 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 playlist; private CountDownLatch onCurrentMediaItemChangedLatch; - PlayerCallbackForPlaylist(List playlist, CountDownLatch latch) { + PlayerCallbackForPlaylist(List 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(); + } } } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java index 09e0325e93..21659637ab 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java @@ -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 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)); } } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java index 1c6cc151c9..ccb29f8ca6 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java @@ -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 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 diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 85d0155bd7..e78c55b2af 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -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); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 57fee20d04..85d3530e2d 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -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); } diff --git a/library/common/proguard-rules.txt b/library/common/proguard-rules.txt index 18e5264c20..8de310a867 100644 --- a/library/common/proguard-rules.txt +++ b/library/common/proguard-rules.txt @@ -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 diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java index c4f4a2bbb5..b4208c5282 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -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; /** diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index b751fff7bd..0caa3d50df 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -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 = diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java index 556b04b8ca..0c25f2d43e 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java @@ -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. * *

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

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

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 drmSessionForClearTypes, @Nullable byte[] keySetId) { + Assertions.checkArgument(!(forceDefaultLicenseUri && licenseUri == null)); this.uuid = uuid; this.licenseUri = licenseUri; this.requestHeaders = requestHeaders; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index d2170b9eab..1c450ca02d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -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 corresponding troubleshooting + * topic. + */ + 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; } - } /** diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 6d5f167047..d6dd67ee7d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -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. * diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index a4e3c1dfbe..413b8c540b 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -515,8 +515,8 @@ public final class ParsableByteArray { * Reads a line of text. * *

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. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 745c44395f..61c762615c 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -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; diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java index 46202a5991..2baac87e85 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java @@ -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(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java index 7b78147e12..0d5e55fc83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java @@ -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. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java index d46b939c1f..25c468330c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java @@ -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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java index 8ff7f50402..3ef38c8520 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java @@ -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(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 7a52aae738..3d1c135a6b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -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. + * + *

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}. + * + *

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

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}. + * + *

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}. + * + *

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}. + * + *

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(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java b/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java index 66216de861..fe7f8c0f40 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java @@ -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; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 4181bba1ca..26ec4b94c2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -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. + * + *

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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 2d034335c8..07c3541552 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -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(); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java index ae65eacd13..5ddedd7bd7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Sonic.java @@ -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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index 5c3c1db0c7..ef5bbbc078 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -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}. + *

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(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 7ab90b023e..6a20cf7bda 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -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 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 requestProperties = new HashMap<>(); // Add standard request properties for supported schemes. String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml" diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index 09fa444cf3..a4cbe17b82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -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() { - @Override - protected Void doWork() throws IOException { - cacheWriter.cache(); - return null; - } + downloadRunnable = + new RunnableFutureTask() { + @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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java index 7859254401..f4a7b89fc7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java @@ -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 entry : drmConfiguration.requestHeaders.entrySet()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index 121eeb940d..a5c8ff631d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -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(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index 20d9f44562..824b8dedaf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 1a451cb0c3..1a5371c83f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -315,6 +315,11 @@ public class ChunkSampleStream 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(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 4ce0ea8df5..907607f859 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -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); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index d15804fd51..94c02c7e83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -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); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java index dc5aefac6d..091c57de10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java @@ -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); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloaderTest.java new file mode 100644 index 0000000000..52d83c133a --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloaderTest.java @@ -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; + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 241834fab5..11a2204f81 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -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(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java index 58b9a853e7..b64466cc00 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java @@ -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). + * + *

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). + * + *

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 findSpan( - SpannedString testObject, int expectedStart, int expectedEnd, Class 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); } } diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 660605ebe5..c8f4cadcb1 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -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: diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 325dc24aec..6e880b905f 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -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; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 58cb57f261..551ebc3ea3 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -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 initializationData = null; @Nullable String mimeType = null; + if (atomType == Atom.TYPE_m1v_) { + mimeType = MimeTypes.VIDEO_MPEG; + } + + @Nullable List initializationData = null; @Nullable String codecs = null; @Nullable byte[] projectionData = null; @C.StereoMode diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 0a9ead7c48..c8ef90742b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -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; } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 5e0709228d..0089f68bf4 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -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 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); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 89e7687a21..2b1ec60607 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -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 diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index f7a99a50dc..4e96d39e7c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -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); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 65a9a5ed8f..1ae7812bd4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -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); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index b52a3e6f82..18862c4103 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -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. * *

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

Action customization

* - * Playback actions can be displayed or omitted as follows: + * Playback actions can be included or omitted as follows: * *
    - *
  • {@code useNavigationActions} - Sets whether the previous and next actions are - * displayed. - *
      - *
    • Corresponding setter: {@link #setUseNavigationActions(boolean)} - *
    • Default: {@code true} - *
    - *
  • {@code useNavigationActionsInCompactView} - Sets whether the previous and next - * actions are displayed in compact view (including the lock screen notification). - *
      - *
    • Corresponding setter: {@link #setUseNavigationActionsInCompactView(boolean)} - *
    • Default: {@code false} - *
    - *
  • {@code usePlayPauseActions} - Sets whether the play and pause actions are displayed. + *
  • {@code usePlayPauseActions} - Sets whether the play and pause actions are used. *
      *
    • Corresponding setter: {@link #setUsePlayPauseActions(boolean)} *
    • Default: {@code true} *
    - *
  • {@code useStopAction} - Sets whether the stop action is displayed. - *
      - *
    • Corresponding setter: {@link #setUseStopAction(boolean)} - *
    • Default: {@code false} - *
    *
  • {@code rewindIncrementMs} - Sets the rewind increment. If set to zero the rewind - * action is not displayed. + * action is not used. *
      *
    • Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)} *
    • Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS} (5000) *
    *
  • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the - * fast forward action is not displayed. + * fast forward action is not used. *
      *
    • Corresponding setter: {@link #setControlDispatcher(ControlDispatcher)} *
    • Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS} (15000) *
    + *
  • {@code usePreviousAction} - Whether the previous action is used. + *
      + *
    • Corresponding setter: {@link #setUsePreviousAction(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code usePreviousActionInCompactView} - 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. + *
      + *
    • Corresponding setter: {@link #setUsePreviousActionInCompactView(boolean)} + *
    • Default: {@code false} + *
    + *
  • {@code useNextAction} - Whether the next action is used. + *
      + *
    • Corresponding setter: {@link #setUseNextAction(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code useNextActionInCompactView} - 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. + *
      + *
    • Corresponding setter: {@link #setUseNextActionInCompactView(boolean)} + *
    • Default: {@code false} + *
    + *
  • {@code useStopAction} - Sets whether the stop action is used. + *
      + *
    • Corresponding setter: {@link #setUseStopAction(boolean)} + *
    • Default: {@code false} + *
    *
* *

Overriding drawables

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

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 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 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); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 049af9b64a..c1587d8cdf 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -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); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index ed2bad6eeb..3cff0ef3cb 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -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())); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java index 38d8bc9710..4ae1b32215 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java @@ -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); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index 8a8f3d3c76..0ae38e83a9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -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); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java index 5080e86345..674826e387 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SceneRenderer.java @@ -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. diff --git a/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml b/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml index b7dc40120a..9fe6651b37 100644 --- a/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml +++ b/library/ui/src/main/res/layout/exo_styled_settings_list_item.xml @@ -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"> diff --git a/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml b/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml index 9d184f3bbe..7f2a42b506 100644 --- a/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml +++ b/library/ui/src/main/res/layout/exo_styled_sub_settings_list_item.xml @@ -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"> diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 439afb19c2..1776030417 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -72,8 +72,16 @@ + + + + + + + + @@ -154,6 +162,7 @@ + @@ -186,6 +195,7 @@ + @@ -217,6 +227,7 @@ + @@ -233,6 +244,7 @@ + diff --git a/library/ui/src/main/res/values/dimens.xml b/library/ui/src/main/res/values/dimens.xml index 93bfd8828d..3c4e998852 100644 --- a/library/ui/src/main/res/values/dimens.xml +++ b/library/ui/src/main/res/values/dimens.xml @@ -38,8 +38,8 @@ 2dp 10dp 14dp - 14dp - 14dp + 48dp + 48dp 52dp 60dp diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 38daccb377..7133b78f70 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -93,12 +93,19 @@ center|bottom @dimen/exo_icon_padding_bottom @style/ExoStyledControls.ButtonText + + @android:color/white + 0dp +