From 7aff4764e4c5c72762e3af46957925082c704ff2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 22 Mar 2023 10:32:40 +0000 Subject: [PATCH 01/35] Add ExoPlayer versions to GH bug template PiperOrigin-RevId: 518523484 --- .github/ISSUE_TEMPLATE/bug.yml | 36 ++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index ed6cef06fd..3851f7b13c 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -18,17 +18,33 @@ body: - type: dropdown attributes: label: Media3 Version - description: What version of Media3 are you using? + description: What version of Media3 (or ExoPlayer) are you using? options: - - 1.0.0 - - 1.0.0-rc02 - - 1.0.0-rc01 - - 1.0.0-beta03 - - 1.0.0-beta02 - - 1.0.0-beta01 - - 1.0.0-alpha03 - - 1.0.0-alpha02 - - 1.0.0-alpha01 + - Media3 1.0.0 + - Media3 1.0.0-rc02 + - Media3 1.0.0-rc01 + - Media3 1.0.0-beta03 + - Media3 1.0.0-beta02 + - Media3 1.0.0-beta01 + - Media3 1.0.0-alpha03 + - Media3 1.0.0-alpha02 + - Media3 1.0.0-alpha01 + - ExoPlayer 2.18.5 + - ExoPlayer 2.18.4 + - ExoPlayer 2.18.3 + - ExoPlayer 2.18.2 + - ExoPlayer 2.18.1 + - ExoPlayer 2.18.0 + - ExoPlayer 2.17.1 + - ExoPlayer 2.17.0 + - ExoPlayer 2.16.1 + - ExoPlayer 2.16.0 + - ExoPlayer 2.15.1 + - ExoPlayer 2.15.0 + - ExoPlayer 2.14.2 + - ExoPlayer 2.14.1 + - ExoPlayer 2.14.0 + - Older (unsupported) validations: required: true - type: textarea From d1d16659a6efc68c9085f850c364dc727ffbf709 Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 30 Mar 2023 14:45:05 +0000 Subject: [PATCH 02/35] Fix javadoc links in media README files Also fixed the javadoc link in devsite and removed javadoc links from decoder extensions as it is not published yet on developer.android.com. #minor-release PiperOrigin-RevId: 520636868 (cherry picked from commit 6a928805d4b0f52f5f07b1f7cd75e2a7357e9397) --- libraries/cast/README.md | 2 +- libraries/common/README.md | 2 +- libraries/database/README.md | 2 +- libraries/datasource/README.md | 2 +- libraries/datasource_cronet/README.md | 2 +- libraries/datasource_okhttp/README.md | 2 +- libraries/datasource_rtmp/README.md | 2 +- libraries/decoder/README.md | 2 +- libraries/decoder_av1/README.md | 7 +------ libraries/decoder_ffmpeg/README.md | 7 +------ libraries/decoder_flac/README.md | 7 +------ libraries/decoder_opus/README.md | 7 +------ libraries/decoder_vp9/README.md | 7 +------ libraries/effect/README.md | 2 +- libraries/exoplayer/README.md | 2 +- libraries/exoplayer_dash/README.md | 2 +- libraries/exoplayer_hls/README.md | 2 +- libraries/exoplayer_ima/README.md | 2 +- libraries/exoplayer_rtsp/README.md | 2 +- libraries/exoplayer_smoothstreaming/README.md | 2 +- libraries/exoplayer_workmanager/README.md | 2 +- libraries/extractor/README.md | 2 +- libraries/test_utils/README.md | 2 +- libraries/test_utils_robolectric/README.md | 2 +- libraries/transformer/README.md | 2 +- libraries/ui/README.md | 2 +- 26 files changed, 26 insertions(+), 51 deletions(-) diff --git a/libraries/cast/README.md b/libraries/cast/README.md index d8b25289b7..a30ac6b84c 100644 --- a/libraries/cast/README.md +++ b/libraries/cast/README.md @@ -32,4 +32,4 @@ UI module. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/cast/package-summary diff --git a/libraries/common/README.md b/libraries/common/README.md index f2ef17bac6..462062c6b6 100644 --- a/libraries/common/README.md +++ b/libraries/common/README.md @@ -7,4 +7,4 @@ will not normally need to depend on this module directly. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/common/package-summary diff --git a/libraries/database/README.md b/libraries/database/README.md index 793664d5ad..27ab3841a7 100644 --- a/libraries/database/README.md +++ b/libraries/database/README.md @@ -7,4 +7,4 @@ will not normally need to depend on this module directly. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/database/package-summary diff --git a/libraries/datasource/README.md b/libraries/datasource/README.md index 4e75082831..c66311bb70 100644 --- a/libraries/datasource/README.md +++ b/libraries/datasource/README.md @@ -8,4 +8,4 @@ depend on this module directly. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/datasource/package-summary diff --git a/libraries/datasource_cronet/README.md b/libraries/datasource_cronet/README.md index 4a5dbbd674..1514707171 100644 --- a/libraries/datasource_cronet/README.md +++ b/libraries/datasource_cronet/README.md @@ -124,4 +124,4 @@ application. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/datasource/cronet/package-summary diff --git a/libraries/datasource_okhttp/README.md b/libraries/datasource_okhttp/README.md index cb62baa0b4..7bd14d9691 100644 --- a/libraries/datasource_okhttp/README.md +++ b/libraries/datasource_okhttp/README.md @@ -53,4 +53,4 @@ new DefaultDataSourceFactory( * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/datasource/okhttp/package-summary diff --git a/libraries/datasource_rtmp/README.md b/libraries/datasource_rtmp/README.md index 27890cff86..a2c189abe2 100644 --- a/libraries/datasource_rtmp/README.md +++ b/libraries/datasource_rtmp/README.md @@ -50,4 +50,4 @@ doesn't need to handle any other protocols, you can update any * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/datasource/rtmp/package-summary diff --git a/libraries/decoder/README.md b/libraries/decoder/README.md index 150fcef72a..70af19ec8f 100644 --- a/libraries/decoder/README.md +++ b/libraries/decoder/README.md @@ -7,4 +7,4 @@ depend on this module directly. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/decoder/package-summary diff --git a/libraries/decoder_av1/README.md b/libraries/decoder_av1/README.md index a4f741490a..7e80d6bcd6 100644 --- a/libraries/decoder_av1/README.md +++ b/libraries/decoder_av1/README.md @@ -124,10 +124,5 @@ gets from the libgav1 decoder: Note: Although the default option uses `ANativeWindow`, based on our testing the GL rendering mode has better performance, so should be preferred -## Links - - -* [Javadoc][] - -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages + diff --git a/libraries/decoder_ffmpeg/README.md b/libraries/decoder_ffmpeg/README.md index a819fc23ad..9dfd4fe8f4 100644 --- a/libraries/decoder_ffmpeg/README.md +++ b/libraries/decoder_ffmpeg/README.md @@ -117,10 +117,5 @@ then implement your own logic to use the renderer for a given track. [ExoPlayer issue 2781]: https://github.com/google/ExoPlayer/issues/2781 [Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension -## Links - - -* [Javadoc][] - -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages + diff --git a/libraries/decoder_flac/README.md b/libraries/decoder_flac/README.md index e381d2f8e1..fa73ddb64f 100644 --- a/libraries/decoder_flac/README.md +++ b/libraries/decoder_flac/README.md @@ -96,10 +96,5 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibflacAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. -## Links - - -* [Javadoc][] - -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages + diff --git a/libraries/decoder_opus/README.md b/libraries/decoder_opus/README.md index 26195664a8..b96e3e7619 100644 --- a/libraries/decoder_opus/README.md +++ b/libraries/decoder_opus/README.md @@ -100,10 +100,5 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibopusAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. -## Links - - -* [Javadoc][] - -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages + diff --git a/libraries/decoder_vp9/README.md b/libraries/decoder_vp9/README.md index e504c7a730..16b102e49b 100644 --- a/libraries/decoder_vp9/README.md +++ b/libraries/decoder_vp9/README.md @@ -137,10 +137,5 @@ gets from the libvpx decoder: Note: Although the default option uses `ANativeWindow`, based on our testing the GL rendering mode has better performance, so should be preferred. -## Links - - -* [Javadoc][] - -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages + diff --git a/libraries/effect/README.md b/libraries/effect/README.md index 532c8f61b8..b3767afaae 100644 --- a/libraries/effect/README.md +++ b/libraries/effect/README.md @@ -22,4 +22,4 @@ locally. Instructions for doing this can be found in the [top level README][]. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/effect/package-summary diff --git a/libraries/exoplayer/README.md b/libraries/exoplayer/README.md index 0fca23f366..76eceefa4c 100644 --- a/libraries/exoplayer/README.md +++ b/libraries/exoplayer/README.md @@ -23,4 +23,4 @@ locally. Instructions for doing this can be found in the [top level README][]. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/package-summary diff --git a/libraries/exoplayer_dash/README.md b/libraries/exoplayer_dash/README.md index 3f7ec5035f..566747d572 100644 --- a/libraries/exoplayer_dash/README.md +++ b/libraries/exoplayer_dash/README.md @@ -40,4 +40,4 @@ instances and pass them directly to the player. For advanced download use cases, * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/dash/package-summary diff --git a/libraries/exoplayer_hls/README.md b/libraries/exoplayer_hls/README.md index f89d324d06..588c3ccfa3 100644 --- a/libraries/exoplayer_hls/README.md +++ b/libraries/exoplayer_hls/README.md @@ -39,4 +39,4 @@ instances and pass them directly to the player. For advanced download use cases, * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/hls/package-summary diff --git a/libraries/exoplayer_ima/README.md b/libraries/exoplayer_ima/README.md index 06ada682a7..0cdad3fbb4 100644 --- a/libraries/exoplayer_ima/README.md +++ b/libraries/exoplayer_ima/README.md @@ -56,4 +56,4 @@ player position when backgrounded during ad playback. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/ima/package-summary diff --git a/libraries/exoplayer_rtsp/README.md b/libraries/exoplayer_rtsp/README.md index f83220fe1d..f1cae579e3 100644 --- a/libraries/exoplayer_rtsp/README.md +++ b/libraries/exoplayer_rtsp/README.md @@ -34,4 +34,4 @@ instances and pass them directly to the player. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/rtsp/package-summary diff --git a/libraries/exoplayer_smoothstreaming/README.md b/libraries/exoplayer_smoothstreaming/README.md index 076985bee7..a5d68152a9 100644 --- a/libraries/exoplayer_smoothstreaming/README.md +++ b/libraries/exoplayer_smoothstreaming/README.md @@ -39,4 +39,4 @@ instances and pass them directly to the player. For advanced download use cases, * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/smoothstreaming/package-summary diff --git a/libraries/exoplayer_workmanager/README.md b/libraries/exoplayer_workmanager/README.md index 7fa6c6d267..d2b3e33fcb 100644 --- a/libraries/exoplayer_workmanager/README.md +++ b/libraries/exoplayer_workmanager/README.md @@ -24,4 +24,4 @@ locally. Instructions for doing this can be found in the [top level README][]. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/workmanager/package-summary diff --git a/libraries/extractor/README.md b/libraries/extractor/README.md index 22b82fa5b5..783bb65092 100644 --- a/libraries/extractor/README.md +++ b/libraries/extractor/README.md @@ -7,4 +7,4 @@ not normally need to depend on this module directly. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/extractor/package-summary diff --git a/libraries/test_utils/README.md b/libraries/test_utils/README.md index f8d78bb573..cdc5fc8269 100644 --- a/libraries/test_utils/README.md +++ b/libraries/test_utils/README.md @@ -6,4 +6,4 @@ Provides utility classes for media unit and instrumentation tests. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/test/utils/package-summary diff --git a/libraries/test_utils_robolectric/README.md b/libraries/test_utils_robolectric/README.md index 4dbd123eee..799bf97a81 100644 --- a/libraries/test_utils_robolectric/README.md +++ b/libraries/test_utils_robolectric/README.md @@ -6,4 +6,4 @@ Provides test infrastructure for Robolectric-based media tests. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/test/utils/robolectric/package-summary diff --git a/libraries/transformer/README.md b/libraries/transformer/README.md index 2edafcd386..af8743be74 100644 --- a/libraries/transformer/README.md +++ b/libraries/transformer/README.md @@ -24,4 +24,4 @@ locally. Instructions for doing this can be found in the [top level README][]. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/transformer/package-summary diff --git a/libraries/ui/README.md b/libraries/ui/README.md index fe864c584d..a5dafe1e22 100644 --- a/libraries/ui/README.md +++ b/libraries/ui/README.md @@ -24,4 +24,4 @@ locally. Instructions for doing this can be found in the [top level README][]. * [Javadoc][] -[Javadoc]: https://developer.android.com/reference/androidx/media3/packages +[Javadoc]: https://developer.android.com/reference/androidx/media3/ui/package-summary From 2ca90501347dd22edd6e2058661292ea75af993f Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 30 Mar 2023 15:48:28 +0000 Subject: [PATCH 03/35] Add media3 guide entries in README files PiperOrigin-RevId: 520650881 (cherry picked from commit 766e7d7d35f20b91a2c72635ac13b10ad9d22dac) --- libraries/decoder_av1/README.md | 7 ++++++- libraries/decoder_ffmpeg/README.md | 7 ++++++- libraries/decoder_flac/README.md | 7 ++++++- libraries/decoder_opus/README.md | 7 ++++++- libraries/decoder_vp9/README.md | 7 ++++++- libraries/exoplayer_dash/README.md | 4 ++-- libraries/exoplayer_hls/README.md | 4 ++-- libraries/exoplayer_ima/README.md | 4 ++-- libraries/exoplayer_rtsp/README.md | 4 ++-- libraries/exoplayer_smoothstreaming/README.md | 4 ++-- libraries/transformer/README.md | 4 ++-- libraries/ui/README.md | 4 ++-- 12 files changed, 44 insertions(+), 19 deletions(-) diff --git a/libraries/decoder_av1/README.md b/libraries/decoder_av1/README.md index 7e80d6bcd6..bdf5083097 100644 --- a/libraries/decoder_av1/README.md +++ b/libraries/decoder_av1/README.md @@ -124,5 +124,10 @@ gets from the libgav1 decoder: Note: Although the default option uses `ANativeWindow`, based on our testing the GL rendering mode has better performance, so should be preferred - +## Links + +* [Troubleshooting using decoding extensions][] + + +[Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playbacks diff --git a/libraries/decoder_ffmpeg/README.md b/libraries/decoder_ffmpeg/README.md index 9dfd4fe8f4..204dd1bba6 100644 --- a/libraries/decoder_ffmpeg/README.md +++ b/libraries/decoder_ffmpeg/README.md @@ -117,5 +117,10 @@ then implement your own logic to use the renderer for a given track. [ExoPlayer issue 2781]: https://github.com/google/ExoPlayer/issues/2781 [Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension - +## Links + +* [Troubleshooting using decoding extensions][] + + +[Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/decoder_flac/README.md b/libraries/decoder_flac/README.md index fa73ddb64f..3599493410 100644 --- a/libraries/decoder_flac/README.md +++ b/libraries/decoder_flac/README.md @@ -96,5 +96,10 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibflacAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. - +## Links + +* [Troubleshooting using decoding extensions][] + + +[Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/decoder_opus/README.md b/libraries/decoder_opus/README.md index b96e3e7619..e879f1a87a 100644 --- a/libraries/decoder_opus/README.md +++ b/libraries/decoder_opus/README.md @@ -100,5 +100,10 @@ a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibopusAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. - +## Links + +* [Troubleshooting using decoding extensions][] + + +[Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/decoder_vp9/README.md b/libraries/decoder_vp9/README.md index 16b102e49b..633d00a794 100644 --- a/libraries/decoder_vp9/README.md +++ b/libraries/decoder_vp9/README.md @@ -137,5 +137,10 @@ gets from the libvpx decoder: Note: Although the default option uses `ANativeWindow`, based on our testing the GL rendering mode has better performance, so should be preferred. - +## Links + +* [Troubleshooting using decoding extensions][] + + +[Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/exoplayer_dash/README.md b/libraries/exoplayer_dash/README.md index 566747d572..f6614a0bb5 100644 --- a/libraries/exoplayer_dash/README.md +++ b/libraries/exoplayer_dash/README.md @@ -36,8 +36,8 @@ instances and pass them directly to the player. For advanced download use cases, ## Links - - +* [Developer Guide][] * [Javadoc][] +[Developer Guide]: https://developer.android.com/guide/topics/media/exoplayer/dash [Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/dash/package-summary diff --git a/libraries/exoplayer_hls/README.md b/libraries/exoplayer_hls/README.md index 588c3ccfa3..39af71b53c 100644 --- a/libraries/exoplayer_hls/README.md +++ b/libraries/exoplayer_hls/README.md @@ -35,8 +35,8 @@ instances and pass them directly to the player. For advanced download use cases, ## Links - - +* [Developer Guide][] * [Javadoc][] +[Developer Guide]: https://developer.android.com/guide/topics/media/exoplayer/hls [Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/hls/package-summary diff --git a/libraries/exoplayer_ima/README.md b/libraries/exoplayer_ima/README.md index 0cdad3fbb4..f7d9750339 100644 --- a/libraries/exoplayer_ima/README.md +++ b/libraries/exoplayer_ima/README.md @@ -52,8 +52,8 @@ player position when backgrounded during ad playback. ## Links - - +* [Developer Guide][] * [Javadoc][] +[Developer Guide]: https://developer.android.com/guide/topics/media/exoplayer/ad-insertion [Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/ima/package-summary diff --git a/libraries/exoplayer_rtsp/README.md b/libraries/exoplayer_rtsp/README.md index f1cae579e3..cd0780cf71 100644 --- a/libraries/exoplayer_rtsp/README.md +++ b/libraries/exoplayer_rtsp/README.md @@ -30,8 +30,8 @@ instances and pass them directly to the player. ## Links - - +* [Developer Guide][] * [Javadoc][] +[Developer Guide]: https://developer.android.com/guide/topics/media/exoplayer/rtsp [Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/rtsp/package-summary diff --git a/libraries/exoplayer_smoothstreaming/README.md b/libraries/exoplayer_smoothstreaming/README.md index a5d68152a9..c888ff6c80 100644 --- a/libraries/exoplayer_smoothstreaming/README.md +++ b/libraries/exoplayer_smoothstreaming/README.md @@ -35,8 +35,8 @@ instances and pass them directly to the player. For advanced download use cases, ## Links - - +* [Developer Guide][] * [Javadoc][] +[Developer Guide]: https://developer.android.com/guide/topics/media/exoplayer/smoothstreaming [Javadoc]: https://developer.android.com/reference/androidx/media3/exoplayer/smoothstreaming/package-summary diff --git a/libraries/transformer/README.md b/libraries/transformer/README.md index af8743be74..c77c33c036 100644 --- a/libraries/transformer/README.md +++ b/libraries/transformer/README.md @@ -20,8 +20,8 @@ locally. Instructions for doing this can be found in the [top level README][]. ## Links - - +* [Developer Guide][] * [Javadoc][] +[Developer Guide]: https://developer.android.com/guide/topics/media/transformer [Javadoc]: https://developer.android.com/reference/androidx/media3/transformer/package-summary diff --git a/libraries/ui/README.md b/libraries/ui/README.md index a5dafe1e22..7cca09aa7f 100644 --- a/libraries/ui/README.md +++ b/libraries/ui/README.md @@ -20,8 +20,8 @@ locally. Instructions for doing this can be found in the [top level README][]. ## Links - - +* [Developer Guide][] * [Javadoc][] +[Developer Guide]: https://developer.android.com/guide/topics/media/exoplayer/ui-components [Javadoc]: https://developer.android.com/reference/androidx/media3/ui/package-summary From 4666d57b1295952ee746058b6965818ae55b4c7a Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 3 Mar 2023 17:32:36 +0000 Subject: [PATCH 04/35] Ensure `ForwardingPlayer` users do listener registration correctly The `@CallSuper` annotation should help catch cases where subclasses are calling `delegate.addListener` instead of `super.addListener` but it will also (unintentionally) prevent subclasses from either completely no-opping the listener registration, or implementing it themselves in a very custom way. I think that's probably OK, since these cases are probably unusual, and they should be able to suppress the warning/error. Issue: androidx/media#258 #minor-release PiperOrigin-RevId: 513848402 (cherry picked from commit 5d23a92923324ecab338d14e865bcc26316aab00) --- .../media3/common/ForwardingPlayer.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java index 957ab7d219..0941df7d5e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java @@ -20,6 +20,7 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import androidx.media3.common.text.Cue; import androidx.media3.common.text.CueGroup; @@ -47,14 +48,29 @@ public class ForwardingPlayer implements Player { return player.getApplicationLooper(); } - /** Calls {@link Player#addListener(Listener)} on the delegate. */ + /** + * Calls {@link Player#addListener(Listener)} on the delegate. + * + *

Overrides of this method must delegate to {@code super.addListener} and not {@code + * delegate.addListener}, in order to ensure the correct {@link Player} instance is passed to + * {@link Player.Listener#onEvents(Player, Events)} (i.e. this forwarding instance, and not the + * underlying {@code delegate} instance). + */ @Override + @CallSuper public void addListener(Listener listener) { player.addListener(new ForwardingListener(this, listener)); } - /** Calls {@link Player#removeListener(Listener)} on the delegate. */ + /** + * Calls {@link Player#removeListener(Listener)} on the delegate. + * + *

Overrides of this method must delegate to {@code super.removeListener} and not {@code + * delegate.removeListener}, in order to ensure the listener 'matches' the listener added via + * {@link #addListener} (otherwise the listener registered on the delegate won't be removed). + */ @Override + @CallSuper public void removeListener(Listener listener) { player.removeListener(new ForwardingListener(this, listener)); } From a94fb21dcb583f38e4510c129ca1ef9679725b33 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 7 Mar 2023 15:05:20 +0000 Subject: [PATCH 05/35] Make DefaultDashChunkSource more robust against empty segment timelines Segment timelines are technically allowed to be empty, but not all places add the necessary checks. Issue: google/ExoPlayer#11014 PiperOrigin-RevId: 514722205 (cherry picked from commit 788132b9dc9f5dd9aa46f413b16f43433c9c7702) --- .../media3/exoplayer/dash/DefaultDashChunkSource.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java index 31fe7ac8dc..d1c598e5ac 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java @@ -236,9 +236,12 @@ public class DefaultDashChunkSource implements DashChunkSource { // Segments are aligned across representations, so any segment index will do. for (RepresentationHolder representationHolder : representationHolders) { if (representationHolder.segmentIndex != null) { + long segmentCount = representationHolder.getSegmentCount(); + if (segmentCount == 0) { + continue; + } long segmentNum = representationHolder.getSegmentNum(positionUs); long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum); - long segmentCount = representationHolder.getSegmentCount(); long secondSyncUs = firstSyncUs < positionUs && (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED @@ -594,7 +597,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } private long getAvailableLiveDurationUs(long nowUnixTimeUs, long playbackPositionUs) { - if (!manifest.dynamic) { + if (!manifest.dynamic || representationHolders[0].getSegmentCount() == 0) { return C.TIME_UNSET; } long lastSegmentNum = representationHolders[0].getLastAvailableSegmentNum(nowUnixTimeUs); From 60e0546bea4254f25a23374dca64ee7c25c72e76 Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Thu, 16 Mar 2023 15:33:38 +0000 Subject: [PATCH 06/35] Merge pull request #11061 from cedricxperi:dts-udts-support PiperOrigin-RevId: 517067549 (cherry picked from commit 49d85d625c42177aaf0d6413ba7ee0ae7b4d580e) --- .../src/main/java/androidx/media3/extractor/mp4/Atom.java | 3 +++ .../main/java/androidx/media3/extractor/mp4/AtomParsers.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Atom.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Atom.java index 6256fa7f27..3f2cb72ad8 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Atom.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Atom.java @@ -176,6 +176,9 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_ddts = 0x64647473; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_udts = 0x75647473; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_tfdt = 0x74666474; diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index 5e290bc67d..d7b615538c 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1560,7 +1560,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // because these streams can carry simultaneously multiple representations of the same // audio. Use stereo by default. channelCount = 2; - } else if (childAtomType == Atom.TYPE_ddts) { + } else if (childAtomType == Atom.TYPE_ddts || childAtomType == Atom.TYPE_udts) { out.format = new Format.Builder() .setId(trackId) From 65d4202f6e0b525d4cb6d1c4a7e587d5fc20f789 Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 16 Mar 2023 11:37:04 +0000 Subject: [PATCH 07/35] Add support to fetch `ColorInfo` from `hvcc` box in `AtomParsers` #minor-release PiperOrigin-RevId: 517086016 (cherry picked from commit 8a5fcf82d32268c81b641951d4d270875cbcc62a) --- .../androidx/media3/extractor/HevcConfig.java | 41 ++++++++++++++++++- .../media3/extractor/NalUnitUtil.java | 35 +++++++++++++--- .../media3/extractor/mp4/AtomParsers.java | 9 ++++ .../media3/extractor/NalUnitUtilTest.java | 3 ++ 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java b/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java index cbe14e9b16..ad2c0851dc 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java @@ -16,6 +16,7 @@ package androidx.media3.extractor; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.ParserException; import androidx.media3.common.util.CodecSpecificDataUtil; @@ -61,6 +62,9 @@ public final class HevcConfig { int bufferPosition = 0; int width = Format.NO_VALUE; int height = Format.NO_VALUE; + @C.ColorSpace int colorSpace = Format.NO_VALUE; + @C.ColorRange int colorRange = Format.NO_VALUE; + @C.ColorTransfer int colorTransfer = Format.NO_VALUE; float pixelWidthHeightRatio = 1; @Nullable String codecs = null; for (int i = 0; i < numberOfArrays; i++) { @@ -84,6 +88,9 @@ public final class HevcConfig { buffer, bufferPosition, bufferPosition + nalUnitLength); width = spsData.width; height = spsData.height; + colorSpace = spsData.colorSpace; + colorRange = spsData.colorRange; + colorTransfer = spsData.colorTransfer; pixelWidthHeightRatio = spsData.pixelWidthHeightRatio; codecs = CodecSpecificDataUtil.buildHevcCodecString( @@ -102,7 +109,15 @@ public final class HevcConfig { List initializationData = csdLength == 0 ? Collections.emptyList() : Collections.singletonList(buffer); return new HevcConfig( - initializationData, lengthSizeMinusOne + 1, width, height, pixelWidthHeightRatio, codecs); + initializationData, + lengthSizeMinusOne + 1, + width, + height, + pixelWidthHeightRatio, + codecs, + colorSpace, + colorRange, + colorTransfer); } catch (ArrayIndexOutOfBoundsException e) { throw ParserException.createForMalformedContainer("Error parsing HEVC config", e); } @@ -129,6 +144,22 @@ public final class HevcConfig { /** The pixel width to height ratio. */ public final float pixelWidthHeightRatio; + /** + * The {@link C.ColorSpace} of the video or {@link Format#NO_VALUE} if unknown or not applicable. + */ + public final @C.ColorSpace int colorSpace; + + /** + * The {@link C.ColorRange} of the video or {@link Format#NO_VALUE} if unknown or not applicable. + */ + public final @C.ColorRange int colorRange; + + /** + * The {@link C.ColorTransfer} of the video or {@link Format#NO_VALUE} if unknown or not + * applicable. + */ + public final @C.ColorTransfer int colorTransfer; + /** * An RFC 6381 codecs string representing the video format, or {@code null} if not known. * @@ -142,12 +173,18 @@ public final class HevcConfig { int width, int height, float pixelWidthHeightRatio, - @Nullable String codecs) { + @Nullable String codecs, + @C.ColorSpace int colorSpace, + @C.ColorRange int colorRange, + @C.ColorTransfer int colorTransfer) { this.initializationData = initializationData; this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; this.width = width; this.height = height; this.pixelWidthHeightRatio = pixelWidthHeightRatio; this.codecs = codecs; + this.colorSpace = colorSpace; + this.colorRange = colorRange; + this.colorTransfer = colorTransfer; } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java index 2a401cbbae..354b59f666 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java @@ -19,6 +19,8 @@ import static java.lang.Math.min; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; +import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; @@ -110,6 +112,9 @@ public final class NalUnitUtil { public final int width; public final int height; public final float pixelWidthHeightRatio; + public final @C.ColorSpace int colorSpace; + public final @C.ColorRange int colorRange; + public final @C.ColorTransfer int colorTransfer; public H265SpsData( int generalProfileSpace, @@ -121,7 +126,10 @@ public final class NalUnitUtil { int seqParameterSetId, int width, int height, - float pixelWidthHeightRatio) { + float pixelWidthHeightRatio, + @C.ColorSpace int colorSpace, + @C.ColorRange int colorRange, + @C.ColorTransfer int colorTransfer) { this.generalProfileSpace = generalProfileSpace; this.generalTierFlag = generalTierFlag; this.generalProfileIdc = generalProfileIdc; @@ -132,6 +140,9 @@ public final class NalUnitUtil { this.width = width; this.height = height; this.pixelWidthHeightRatio = pixelWidthHeightRatio; + this.colorSpace = colorSpace; + this.colorRange = colorRange; + this.colorTransfer = colorTransfer; } } @@ -488,6 +499,10 @@ public final class NalUnitUtil { public static H265SpsData parseH265SpsNalUnitPayload( byte[] nalData, int nalOffset, int nalLimit) { ParsableNalUnitBitArray data = new ParsableNalUnitBitArray(nalData, nalOffset, nalLimit); + // HDR related metadata. + @C.ColorSpace int colorSpace = Format.NO_VALUE; + @C.ColorRange int colorRange = Format.NO_VALUE; + @C.ColorTransfer int colorTransfer = Format.NO_VALUE; data.skipBits(4); // sps_video_parameter_set_id int maxSubLayersMinus1 = data.readBits(3); data.skipBit(); // sps_temporal_id_nesting_flag @@ -594,10 +609,17 @@ public final class NalUnitUtil { data.skipBit(); // overscan_appropriate_flag } if (data.readBit()) { // video_signal_type_present_flag - data.skipBits(4); // video_format, video_full_range_flag + data.skipBits(3); // video_format + boolean fullRangeFlag = data.readBit(); // video_full_range_flag if (data.readBit()) { // colour_description_present_flag - // colour_primaries, transfer_characteristics, matrix_coeffs - data.skipBits(24); + int colorPrimaries = data.readBits(8); // colour_primaries + int transferCharacteristics = data.readBits(8); // transfer_characteristics + data.skipBits(8); // matrix_coeffs + + colorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries); + colorRange = fullRangeFlag ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; + colorTransfer = + ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics); } } if (data.readBit()) { // chroma_loc_info_present_flag @@ -622,7 +644,10 @@ public final class NalUnitUtil { seqParameterSetId, frameWidth, frameHeight, - pixelWidthHeightRatio); + pixelWidthHeightRatio, + colorSpace, + colorRange, + colorTransfer); } /** diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index d7b615538c..0b8f0004ab 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1164,6 +1164,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; pixelWidthHeightRatio = hevcConfig.pixelWidthHeightRatio; } codecs = hevcConfig.codecs; + // Modify these values only if they have not already been set. If 'Atom.TYPE_colr' atom is + // present, these values may be overridden. + if (colorSpace == Format.NO_VALUE + && colorRange == Format.NO_VALUE + && colorTransfer == Format.NO_VALUE) { + colorSpace = hevcConfig.colorSpace; + colorRange = hevcConfig.colorRange; + colorTransfer = hevcConfig.colorTransfer; + } } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { @Nullable DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); if (dolbyVisionConfig != null) { diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java index 59dd8543db..dd5f108bd1 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java @@ -194,6 +194,9 @@ public final class NalUnitUtilTest { assertThat(spsData.pixelWidthHeightRatio).isEqualTo(1); assertThat(spsData.seqParameterSetId).isEqualTo(0); assertThat(spsData.width).isEqualTo(3840); + assertThat(spsData.colorSpace).isEqualTo(6); + assertThat(spsData.colorRange).isEqualTo(2); + assertThat(spsData.colorTransfer).isEqualTo(6); } private static byte[] buildTestData() { From b70b320c49fade04f81e0370318084fd176334e5 Mon Sep 17 00:00:00 2001 From: Rohit Singh Date: Thu, 16 Mar 2023 15:37:02 +0000 Subject: [PATCH 08/35] Merge pull request #11064 from haixia-meta:release-v2 PiperOrigin-RevId: 517128752 (cherry picked from commit 0e3f407e0ec482eecd65b070fbec2d0fe91e255d) --- .../media3/extractor/mp4/AtomParsers.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index 0b8f0004ab..3d2a61dcec 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1182,6 +1182,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else if (childAtomType == Atom.TYPE_vpcC) { ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null); mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9; + parent.setPosition(childStartPosition + Atom.FULL_HEADER_SIZE); + // See vpcC atom syntax: https://www.webmproject.org/vp9/mp4/#syntax_1 + parent.skipBytes(2); // profile(8), level(8) + boolean fullRangeFlag = (parent.readUnsignedByte() & 1) != 0; + int colorPrimaries = parent.readUnsignedByte(); + int transferCharacteristics = parent.readUnsignedByte(); + // Modify these values only if they have not already been set. If 'Atom.TYPE_colr' atom is + // present, these values may be overridden. + if (colorSpace == Format.NO_VALUE + && colorRange == Format.NO_VALUE + && colorTransfer == Format.NO_VALUE) { + colorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries); + colorRange = fullRangeFlag ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; + colorTransfer = + ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics); + } } else if (childAtomType == Atom.TYPE_av1C) { ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null); mimeType = MimeTypes.VIDEO_AV1; From eea37031d1f8809688d9c9a47562d389fed4fb65 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Thu, 23 Mar 2023 09:54:54 +0000 Subject: [PATCH 09/35] Retry RTSP Setup with TCP if response with UDP is UnsupportedTransport If RTSP Setup Request with UDP receives HTTP Error Status 461 UnsupportedTransport, then client will retry with TCP. Issue: google/ExoPlayer#11069 PiperOrigin-RevId: 518807829 (cherry picked from commit ecf4d8b89193e8e3bb3ad3dbc9c7983fd26765ae) --- .../media3/exoplayer/rtsp/RtspClient.java | 21 +- .../exoplayer/rtsp/RtspMediaPeriod.java | 12 +- .../exoplayer/rtsp/RtspMediaSource.java | 9 +- .../media3/exoplayer/rtsp/RtspClientTest.java | 73 +++++ .../exoplayer/rtsp/RtspPlaybackTest.java | 293 ++++++++++++++---- 5 files changed, 342 insertions(+), 66 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java index c8ac907a8d..751731fce6 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java @@ -49,6 +49,7 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.rtsp.RtspMediaPeriod.RtpLoadInfo; import androidx.media3.exoplayer.rtsp.RtspMediaSource.RtspPlaybackException; +import androidx.media3.exoplayer.rtsp.RtspMediaSource.RtspUdpUnsupportedTransportException; import androidx.media3.exoplayer.rtsp.RtspMessageChannel.InterleavedBinaryDataListener; import androidx.media3.exoplayer.rtsp.RtspMessageUtil.RtspAuthUserInfo; import androidx.media3.exoplayer.rtsp.RtspMessageUtil.RtspSessionHeader; @@ -577,8 +578,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; receivedAuthorizationRequest = true; return; } - // fall through: if unauthorized and no userInfo present, or previous authentication - // unsuccessful. + // if unauthorized and no userInfo present, or previous authentication + // unsuccessful, then dispatch RtspPlaybackException + dispatchRtspError( + new RtspPlaybackException( + RtspMessageUtil.toMethodString(requestMethod) + " " + response.status)); + return; + case 461: + String exceptionMessage = + RtspMessageUtil.toMethodString(requestMethod) + " " + response.status; + // If request was SETUP with UDP transport protocol, then throw + // RtspUdpUnsupportedTransportException. + String transportHeaderValue = + checkNotNull(matchingRequest.headers.get(RtspHeaders.TRANSPORT)); + dispatchRtspError( + requestMethod == METHOD_SETUP && !transportHeaderValue.contains("TCP") + ? new RtspUdpUnsupportedTransportException(exceptionMessage) + : new RtspPlaybackException(exceptionMessage)); + return; default: dispatchRtspError( new RtspPlaybackException( diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java index 9dd40cba7c..1af0f2b415 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java @@ -518,7 +518,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // using TCP. Retrying will setup new loadables, so will not retry with the current // loadables. retryWithRtpTcp(); - isUsingRtpTcp = true; } return; } @@ -644,7 +643,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void onPlaybackError(RtspPlaybackException error) { - playbackException = error; + if (error instanceof RtspMediaSource.RtspUdpUnsupportedTransportException && !isUsingRtpTcp) { + // Retry playback with TCP if we receive RtspUdpUnsupportedTransportException, and we are + // not already using TCP. Retrying will setup new loadables. + retryWithRtpTcp(); + } else { + playbackException = error; + } } @Override @@ -668,6 +673,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void retryWithRtpTcp() { + // Retry should only run once. + isUsingRtpTcp = true; + rtspClient.retryWithRtpTcp(); @Nullable diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java index f2f6b52e61..0c69fe0df9 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java @@ -192,7 +192,7 @@ public final class RtspMediaSource extends BaseMediaSource { } /** Thrown when an exception or error is encountered during loading an RTSP stream. */ - public static final class RtspPlaybackException extends IOException { + public static class RtspPlaybackException extends IOException { public RtspPlaybackException(String message) { super(message); } @@ -206,6 +206,13 @@ public final class RtspMediaSource extends BaseMediaSource { } } + /** Thrown when an RTSP Unsupported Transport error (461) is encountered during RTSP Setup. */ + public static final class RtspUdpUnsupportedTransportException extends RtspPlaybackException { + public RtspUdpUnsupportedTransportException(String message) { + super(message); + } + } + private final MediaItem mediaItem; private final RtpDataChannel.Factory rtpDataChannelFactory; private final String userAgent; diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspClientTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspClientTest.java index 104f6ae9c3..e699de2ea7 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspClientTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspClientTest.java @@ -453,4 +453,77 @@ public final class RtspClientTest { RobolectricUtil.runMainLooperUntil(timelineRequestFailed::get); assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); } + + @Test + public void connectServerAndClient_describeResponseRequiresAuthentication_doesNotUpdateTimeline() + throws Exception { + class ResponseProvider implements RtspServer.ResponseProvider { + @Override + public RtspResponse getOptionsResponse() { + return new RtspResponse( + /* status= */ 200, + new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE").build()); + } + + @Override + public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { + String authorizationHeader = headers.get(RtspHeaders.AUTHORIZATION); + if (authorizationHeader == null) { + return new RtspResponse( + /* status= */ 401, + new RtspHeaders.Builder() + .add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ)) + .add( + RtspHeaders.WWW_AUTHENTICATE, + "Digest realm=\"RTSP server\"," + + " nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\"," + + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"") + .add(RtspHeaders.WWW_AUTHENTICATE, "BASIC realm=\"WallyWorld\"") + .build()); + } + if (!authorizationHeader.contains("Digest")) { + return new RtspResponse( + 401, + new RtspHeaders.Builder() + .add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ)) + .build()); + } + + return RtspTestUtils.newDescribeResponseWithSdpMessage( + "v=0\r\n" + + "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n" + + "s=Exoplayer test\r\n" + + "t=0 0\r\n" + // The session is 50.46s long. + + "a=range:npt=0-50.46\r\n", + rtpPacketStreamDumps, + requestedUri); + } + } + rtspServer = new RtspServer(new ResponseProvider()); + + AtomicBoolean timelineRequestFailed = new AtomicBoolean(); + rtspClient = + new RtspClient( + new SessionInfoListener() { + @Override + public void onSessionTimelineUpdated( + RtspSessionTiming timing, ImmutableList tracks) {} + + @Override + public void onSessionTimelineRequestFailed( + String message, @Nullable Throwable cause) { + timelineRequestFailed.set(true); + } + }, + EMPTY_PLAYBACK_LISTENER, + /* userAgent= */ "ExoPlayer:RtspClientTest", + RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + SocketFactory.getDefault(), + /* debugLoggingEnabled= */ false); + rtspClient.start(); + + RobolectricUtil.runMainLooperUntil(timelineRequestFailed::get); + assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); + } } diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java index dc44ce154c..37650c297b 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.rtsp; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static com.google.common.truth.Truth.assertThat; import static java.lang.Math.min; @@ -42,11 +43,13 @@ import androidx.media3.test.utils.robolectric.TestPlayerRunHelper; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicReference; import javax.net.SocketFactory; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -58,30 +61,20 @@ import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) public final class RtspPlaybackTest { + private static final long DEFAULT_TIMEOUT_MS = 8000; private static final String SESSION_DESCRIPTION = "v=0\r\n" + "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n" + "s=Exoplayer test\r\n" + "t=0 0\r\n"; - private final Context applicationContext; - private final CapturingRenderersFactory capturingRenderersFactory; - private final Clock clock; - private final FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel; - private final RtpDataChannel.Factory rtpDataChannelFactory; - + private Context applicationContext; + private CapturingRenderersFactory capturingRenderersFactory; + private Clock clock; private RtpPacketStreamDump aacRtpPacketStreamDump; // ExoPlayer does not support extracting MP4A-LATM RTP payload at the moment. private RtpPacketStreamDump mpeg2tsRtpPacketStreamDump; - - /** Creates a new instance. */ - public RtspPlaybackTest() { - applicationContext = ApplicationProvider.getApplicationContext(); - capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); - clock = new FakeClock(/* isAutoAdvancing= */ true); - fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel(); - rtpDataChannelFactory = (trackId) -> fakeRtpDataChannel; - } + private RtspServer rtspServer; @Rule public ShadowMediaCodecConfig mediaCodecConfig = @@ -89,61 +82,162 @@ public final class RtspPlaybackTest { @Before public void setUp() throws Exception { + applicationContext = ApplicationProvider.getApplicationContext(); + capturingRenderersFactory = new CapturingRenderersFactory(applicationContext); + clock = new FakeClock(/* isAutoAdvancing= */ true); aacRtpPacketStreamDump = RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json"); mpeg2tsRtpPacketStreamDump = RtspTestUtils.readRtpPacketStreamDump("media/rtsp/mpeg2ts-dump.json"); } + @After + public void tearDown() { + Util.closeQuietly(rtspServer); + } + @Test public void prepare_withSupportedTrack_playsTrackUntilEnded() throws Exception { + FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel(); + RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeRtpDataChannel; ResponseProvider responseProvider = new ResponseProvider( clock, ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), fakeRtpDataChannel); + rtspServer = new RtspServer(responseProvider); + ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); - try (RtspServer rtspServer = new RtspServer(responseProvider)) { - ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); + player.prepare(); + player.play(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); - PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); - player.prepare(); - player.play(); - TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); - player.release(); - - // Only setup the supported track (aac). - assertThat(responseProvider.getDumpsForSetUpTracks()).containsExactly(aacRtpPacketStreamDump); - DumpFileAsserts.assertOutput( - applicationContext, playbackOutput, "playbackdumps/rtsp/aac.dump"); - } + // Only setup the supported track (aac). + assertThat(responseProvider.getDumpsForSetUpTracks()).containsExactly(aacRtpPacketStreamDump); + DumpFileAsserts.assertOutput(applicationContext, playbackOutput, "playbackdumps/rtsp/aac.dump"); } @Test public void prepare_noSupportedTrack_throwsPreparationError() throws Exception { - - try (RtspServer rtspServer = + FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel(); + RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeRtpDataChannel; + rtspServer = new RtspServer( new ResponseProvider( - clock, ImmutableList.of(mpeg2tsRtpPacketStreamDump), fakeRtpDataChannel))) { - ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); + clock, ImmutableList.of(mpeg2tsRtpPacketStreamDump), fakeRtpDataChannel)); + ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); - AtomicReference playbackError = new AtomicReference<>(); - player.prepare(); - player.addListener( - new Listener() { - @Override - public void onPlayerError(PlaybackException error) { - playbackError.set(error); - } - }); - RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null); - player.release(); + AtomicReference playbackError = new AtomicReference<>(); + player.prepare(); + player.addListener( + new Listener() { + @Override + public void onPlayerError(PlaybackException error) { + playbackError.set(error); + } + }); + RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null); + player.release(); - assertThat(playbackError.get()) - .hasCauseThat() - .hasMessageThat() - .contains("No playable track."); - } + assertThat(playbackError.get()).hasCauseThat().hasMessageThat().contains("No playable track."); + } + + @Test + public void prepare_withUdpUnsupportedWithFallback_fallsbackToTcpAndPlaysUntilEnd() + throws Exception { + FakeTcpDataSourceRtpDataChannel fakeTcpRtpDataChannel = new FakeTcpDataSourceRtpDataChannel(); + RtpDataChannel.Factory rtpTcpDataChannelFactory = (trackId) -> fakeTcpRtpDataChannel; + ResponseProviderSupportingOnlyTcp responseProviderSupportingOnlyTcp = + new ResponseProviderSupportingOnlyTcp( + clock, + ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), + fakeTcpRtpDataChannel); + ForwardingRtpDataChannelFactory forwardingRtpDataChannelFactory = + new ForwardingRtpDataChannelFactory( + new UdpDataSourceRtpDataChannelFactory(DEFAULT_TIMEOUT_MS), rtpTcpDataChannelFactory); + rtspServer = new RtspServer(responseProviderSupportingOnlyTcp); + ExoPlayer player = + createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); + + PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); + player.prepare(); + player.play(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + // Only setup the supported track (aac). + assertThat(responseProviderSupportingOnlyTcp.getDumpsForSetUpTracks()) + .containsExactly(aacRtpPacketStreamDump); + DumpFileAsserts.assertOutput(applicationContext, playbackOutput, "playbackdumps/rtsp/aac.dump"); + } + + @Test + public void prepare_withUdpUnsupportedWithoutFallback_throwsRtspPlaybackException() + throws Exception { + FakeUdpDataSourceRtpDataChannel fakeUdpRtpDataChannel = new FakeUdpDataSourceRtpDataChannel(); + RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeUdpRtpDataChannel; + ResponseProviderSupportingOnlyTcp responseProvider = + new ResponseProviderSupportingOnlyTcp( + clock, + ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), + fakeUdpRtpDataChannel); + rtspServer = new RtspServer(responseProvider); + ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); + + AtomicReference playbackError = new AtomicReference<>(); + player.prepare(); + player.addListener( + new Listener() { + @Override + public void onPlayerError(PlaybackException error) { + playbackError.set(error); + } + }); + RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null); + player.release(); + + assertThat(playbackError.get()) + .hasCauseThat() + .isInstanceOf(RtspMediaSource.RtspPlaybackException.class); + assertThat(playbackError.get()) + .hasCauseThat() + .hasMessageThat() + .contains("No fallback data channel factory for TCP retry"); + } + + @Test + public void prepare_withUdpUnsupportedWithUdpFallback_throwsRtspUdpUnsupportedTransportException() + throws Exception { + FakeUdpDataSourceRtpDataChannel fakeUdpRtpDataChannel = new FakeUdpDataSourceRtpDataChannel(); + RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeUdpRtpDataChannel; + ResponseProviderSupportingOnlyTcp responseProviderSupportingOnlyTcp = + new ResponseProviderSupportingOnlyTcp( + clock, + ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), + fakeUdpRtpDataChannel); + ForwardingRtpDataChannelFactory forwardingRtpDataChannelFactory = + new ForwardingRtpDataChannelFactory(rtpDataChannelFactory, rtpDataChannelFactory); + rtspServer = new RtspServer(responseProviderSupportingOnlyTcp); + ExoPlayer player = + createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); + + AtomicReference playbackError = new AtomicReference<>(); + player.prepare(); + player.addListener( + new Listener() { + @Override + public void onPlayerError(PlaybackException error) { + playbackError.set(error); + } + }); + RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null); + player.release(); + + assertThat(playbackError.get()) + .hasCauseThat() + .isInstanceOf(RtspMediaSource.RtspUdpUnsupportedTransportException.class); + assertThat(playbackError.get()).hasCauseThat().hasMessageThat().isEqualTo("SETUP 461"); } private ExoPlayer createExoPlayer( @@ -163,16 +257,16 @@ public final class RtspPlaybackTest { return player; } - private static final class ResponseProvider implements RtspServer.ResponseProvider { + private static class ResponseProvider implements RtspServer.ResponseProvider { - private static final String SESSION_ID = "00000000"; + protected static final String SESSION_ID = "00000000"; - private final Clock clock; - private final ArrayList dumpsForSetUpTracks; - private final ImmutableList rtpPacketStreamDumps; + protected final Clock clock; + protected final ArrayList dumpsForSetUpTracks; + protected final ImmutableList rtpPacketStreamDumps; private final RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener; - private RtpPacketTransmitter packetTransmitter; + protected RtpPacketTransmitter packetTransmitter; /** * Creates a new instance. @@ -240,22 +334,54 @@ public final class RtspPlaybackTest { } } - private static final class FakeUdpDataSourceRtpDataChannel extends BaseDataSource - implements RtpDataChannel, RtspMessageChannel.InterleavedBinaryDataListener { + private static final class ResponseProviderSupportingOnlyTcp extends ResponseProvider { - private static final int LOCAL_PORT = 40000; + /** + * Creates a new instance. + * + * @param clock The {@link Clock} used in the test. + * @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}. + * @param binaryDataListener A {@link RtspMessageChannel.InterleavedBinaryDataListener} to send + * RTP data. + */ + public ResponseProviderSupportingOnlyTcp( + Clock clock, + List rtpPacketStreamDumps, + RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) { + super(clock, rtpPacketStreamDumps, binaryDataListener); + } + + @Override + public RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) { + String transportHeaderValue = checkNotNull(headers.get(RtspHeaders.TRANSPORT)); + if (!transportHeaderValue.contains("TCP")) { + return new RtspResponse( + /* status= */ 461, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build()); + } + for (RtpPacketStreamDump rtpPacketStreamDump : rtpPacketStreamDumps) { + if (requestedUri.toString().contains(rtpPacketStreamDump.trackName)) { + dumpsForSetUpTracks.add(rtpPacketStreamDump); + packetTransmitter = new RtpPacketTransmitter(rtpPacketStreamDump, clock); + } + } + return new RtspResponse( + /* status= */ 200, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build()); + } + } + + private abstract static class FakeBaseDataSourceRtpDataChannel extends BaseDataSource + implements RtpDataChannel, RtspMessageChannel.InterleavedBinaryDataListener { + protected static final int LOCAL_PORT = 40000; private final ConcurrentLinkedQueue packetQueue; - public FakeUdpDataSourceRtpDataChannel() { + public FakeBaseDataSourceRtpDataChannel() { super(/* isNetwork= */ false); packetQueue = new ConcurrentLinkedQueue<>(); } @Override - public String getTransport() { - return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1); - } + public abstract String getTransport(); @Override public int getLocalPort() { @@ -307,4 +433,49 @@ public final class RtspPlaybackTest { return byteToRead; } } + + private static final class FakeUdpDataSourceRtpDataChannel + extends FakeBaseDataSourceRtpDataChannel { + @Override + public String getTransport() { + return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1); + } + + @Override + public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() { + return null; + } + } + + private static final class FakeTcpDataSourceRtpDataChannel + extends FakeBaseDataSourceRtpDataChannel { + @Override + public String getTransport() { + return Util.formatInvariant( + "RTP/AVP/TCP;unicast;interleaved=%d-%d", LOCAL_PORT + 2, LOCAL_PORT + 3); + } + } + + private static class ForwardingRtpDataChannelFactory implements RtpDataChannel.Factory { + + private final RtpDataChannel.Factory rtpChannelFactory; + private final RtpDataChannel.Factory rtpFallbackChannelFactory; + + public ForwardingRtpDataChannelFactory( + RtpDataChannel.Factory rtpChannelFactory, + RtpDataChannel.Factory rtpFallbackChannelFactory) { + this.rtpChannelFactory = rtpChannelFactory; + this.rtpFallbackChannelFactory = rtpFallbackChannelFactory; + } + + @Override + public RtpDataChannel createAndOpenDataChannel(int trackId) throws IOException { + return rtpChannelFactory.createAndOpenDataChannel(trackId); + } + + @Override + public RtpDataChannel.Factory createFallbackDataChannelFactory() { + return rtpFallbackChannelFactory; + } + } } From 690ac23a20ec365cfba611430fd45b1c310a93aa Mon Sep 17 00:00:00 2001 From: Tianyi Feng Date: Thu, 30 Mar 2023 17:06:46 +0000 Subject: [PATCH 10/35] Merge pull request #11051 from TiVo:p-fix-for-issue-11050 PiperOrigin-RevId: 518953648 (cherry picked from commit dc3481fca7f1a20f0e3d4b7da4df2bd2701861de) --- .../media3/exoplayer/ExoPlayerImplInternal.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index e84ea1e7e0..3f7a467247 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -1239,7 +1239,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* newPeriodId= */ periodId, /* oldTimeline= */ playbackInfo.timeline, /* oldPeriodId= */ playbackInfo.periodId, - /* positionForTargetOffsetOverrideUs= */ requestedContentPositionUs); + /* positionForTargetOffsetOverrideUs= */ requestedContentPositionUs, + /* forceSetTargetOffsetOverride= */ true); } } finally { playbackInfo = @@ -1882,7 +1883,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* oldPeriodId= */ playbackInfo.periodId, /* positionForTargetOffsetOverrideUs */ positionUpdate.setTargetLiveOffset ? newPositionUs - : C.TIME_UNSET); + : C.TIME_UNSET, + /* forceSetTargetOffsetOverride= */ false); if (periodPositionChanged || newRequestedContentPositionUs != playbackInfo.requestedContentPositionUs) { Object oldPeriodUid = playbackInfo.periodId.periodUid; @@ -1920,7 +1922,8 @@ import java.util.concurrent.atomic.AtomicBoolean; MediaPeriodId newPeriodId, Timeline oldTimeline, MediaPeriodId oldPeriodId, - long positionForTargetOffsetOverrideUs) + long positionForTargetOffsetOverrideUs, + boolean forceSetTargetOffsetOverride) throws ExoPlaybackException { if (!shouldUseLivePlaybackSpeedControl(newTimeline, newPeriodId)) { // Live playback speed control is unused for the current period, reset speed to user-defined @@ -1950,8 +1953,9 @@ import java.util.concurrent.atomic.AtomicBoolean; int oldWindowIndex = oldTimeline.getPeriodByUid(oldPeriodId.periodUid, period).windowIndex; oldWindowUid = oldTimeline.getWindow(oldWindowIndex, window).uid; } - if (!Util.areEqual(oldWindowUid, windowUid)) { - // Reset overridden target live offset to media values if window changes. + if (!Util.areEqual(oldWindowUid, windowUid) || forceSetTargetOffsetOverride) { + // Reset overridden target live offset to media values if window changes or if seekTo + // default live position. livePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(C.TIME_UNSET); } } @@ -2074,7 +2078,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* newPeriodId= */ readingPeriodHolder.info.id, /* oldTimeline= */ playbackInfo.timeline, /* oldPeriodId= */ oldReadingPeriodHolder.info.id, - /* positionForTargetOffsetOverrideUs= */ C.TIME_UNSET); + /* positionForTargetOffsetOverrideUs= */ C.TIME_UNSET, + /* forceSetTargetOffsetOverride= */ false); if (readingPeriodHolder.prepared && readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { From 56dd0f761d158c34370d842016e685fbc44a493f Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 24 Mar 2023 20:39:40 +0000 Subject: [PATCH 11/35] Handle output format changes for empty sample streams correctly When MediaCodecRenderer is given an empty sample stream, it puts its output format change tracking in a bad state where we never process future stream changes because we are waiting for a sample that doesn't exist. We can fix this by: - Looping the pending output stream changes to see if we processed more than one change at once (this fixes the tracking for empty sample streams that are not the first in the queue). - Checking if none of the previous streams queued any samples in onStreamChanged to handle this in the same way as the case where we already output all samples (this fixes the problem when the empty sample stream comes first in the queue). - Also calling onProcessedStreamChange for the case above, which was missing previously. #minor-release PiperOrigin-RevId: 519226637 (cherry picked from commit b9790e69d7649d3399b9b1f920aa417ba4cc38c1) --- .../mediacodec/MediaCodecRenderer.java | 20 +++- .../mediacodec/MediaCodecRendererTest.java | 109 ++++++++++++++++++ 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index 1263bad907..fddfea4e2a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -642,14 +642,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) throws ExoPlaybackException { - if (outputStreamInfo.streamOffsetUs == C.TIME_UNSET - || (pendingOutputStreamChanges.isEmpty() - && lastProcessedOutputBufferTimeUs != C.TIME_UNSET - && lastProcessedOutputBufferTimeUs >= largestQueuedPresentationTimeUs)) { - // This is the first stream, or the previous has been fully output already. + if (outputStreamInfo.streamOffsetUs == C.TIME_UNSET) { + // This is the first stream. setOutputStreamInfo( new OutputStreamInfo( /* previousStreamLastBufferTimeUs= */ C.TIME_UNSET, startPositionUs, offsetUs)); + } else if (pendingOutputStreamChanges.isEmpty() + && (largestQueuedPresentationTimeUs == C.TIME_UNSET + || (lastProcessedOutputBufferTimeUs != C.TIME_UNSET + && lastProcessedOutputBufferTimeUs >= largestQueuedPresentationTimeUs))) { + // All previous streams have never queued any samples or have been fully output already. + setOutputStreamInfo( + new OutputStreamInfo( + /* previousStreamLastBufferTimeUs= */ C.TIME_UNSET, startPositionUs, offsetUs)); + if (outputStreamInfo.streamOffsetUs != C.TIME_UNSET) { + onProcessedStreamChange(); + } } else { pendingOutputStreamChanges.add( new OutputStreamInfo(largestQueuedPresentationTimeUs, startPositionUs, offsetUs)); @@ -1581,7 +1589,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @CallSuper protected void onProcessedOutputBuffer(long presentationTimeUs) { lastProcessedOutputBufferTimeUs = presentationTimeUs; - if (!pendingOutputStreamChanges.isEmpty() + while (!pendingOutputStreamChanges.isEmpty() && presentationTimeUs >= pendingOutputStreamChanges.peek().previousStreamLastBufferTimeUs) { setOutputStreamInfo(pendingOutputStreamChanges.poll()); onProcessedStreamChange(); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java index ac86669ee5..b27a3d3106 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/mediacodec/MediaCodecRendererTest.java @@ -214,6 +214,115 @@ public class MediaCodecRendererTest { inOrder.verify(renderer).onProcessedOutputBuffer(600); } + @Test + public void + render_withReplaceStreamAfterInitialEmptySampleStream_triggersOutputCallbacksInCorrectOrder() + throws Exception { + Format format1 = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).setAverageBitrate(1000).build(); + Format format2 = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).setAverageBitrate(1500).build(); + FakeSampleStream fakeSampleStream1 = createFakeSampleStream(format1 /* no samples */); + FakeSampleStream fakeSampleStream2 = + createFakeSampleStream(format2, /* sampleTimesUs...= */ 0, 100, 200); + MediaCodecRenderer renderer = spy(new TestRenderer()); + renderer.init(/* index= */ 0, PlayerId.UNSET); + + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {format1}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0); + renderer.start(); + long positionUs = 0; + while (!renderer.hasReadStreamToEnd()) { + renderer.render(positionUs, SystemClock.elapsedRealtime()); + positionUs += 100; + } + renderer.replaceStream( + new Format[] {format2}, fakeSampleStream2, /* startPositionUs= */ 0, /* offsetUs= */ 0); + renderer.setCurrentStreamFinal(); + while (!renderer.isEnded()) { + renderer.render(positionUs, SystemClock.elapsedRealtime()); + positionUs += 100; + } + + InOrder inOrder = inOrder(renderer); + inOrder.verify(renderer).onOutputStreamOffsetUsChanged(0); + inOrder.verify(renderer).onOutputStreamOffsetUsChanged(0); + inOrder.verify(renderer).onProcessedStreamChange(); + inOrder.verify(renderer).onOutputFormatChanged(eq(format2), any()); + inOrder.verify(renderer).onProcessedOutputBuffer(0); + inOrder.verify(renderer).onProcessedOutputBuffer(100); + inOrder.verify(renderer).onProcessedOutputBuffer(200); + } + + @Test + public void + render_withReplaceStreamAfterIntermittentEmptySampleStream_triggersOutputCallbacksInCorrectOrder() + throws Exception { + Format format1 = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).setAverageBitrate(1000).build(); + Format format2 = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).setAverageBitrate(1500).build(); + Format format3 = + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).setAverageBitrate(2000).build(); + FakeSampleStream fakeSampleStream1 = + createFakeSampleStream(format1, /* sampleTimesUs...= */ 0, 100); + FakeSampleStream fakeSampleStream2 = createFakeSampleStream(format2 /* no samples */); + FakeSampleStream fakeSampleStream3 = + createFakeSampleStream(format3, /* sampleTimesUs...= */ 0, 100, 200); + MediaCodecRenderer renderer = spy(new TestRenderer()); + renderer.init(/* index= */ 0, PlayerId.UNSET); + + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {format1}, + fakeSampleStream1, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0); + renderer.start(); + long positionUs = 0; + while (!renderer.hasReadStreamToEnd()) { + renderer.render(positionUs, SystemClock.elapsedRealtime()); + positionUs += 100; + } + renderer.replaceStream( + new Format[] {format2}, fakeSampleStream2, /* startPositionUs= */ 200, /* offsetUs= */ 200); + while (!renderer.hasReadStreamToEnd()) { + renderer.render(positionUs, SystemClock.elapsedRealtime()); + positionUs += 100; + } + renderer.replaceStream( + new Format[] {format3}, fakeSampleStream3, /* startPositionUs= */ 200, /* offsetUs= */ 200); + renderer.setCurrentStreamFinal(); + while (!renderer.isEnded()) { + renderer.render(positionUs, SystemClock.elapsedRealtime()); + positionUs += 100; + } + + InOrder inOrder = inOrder(renderer); + inOrder.verify(renderer).onOutputStreamOffsetUsChanged(0); + inOrder.verify(renderer).onOutputFormatChanged(eq(format1), any()); + inOrder.verify(renderer).onProcessedOutputBuffer(0); + inOrder.verify(renderer).onProcessedOutputBuffer(100); + inOrder.verify(renderer).onOutputStreamOffsetUsChanged(200); + inOrder.verify(renderer).onProcessedStreamChange(); + inOrder.verify(renderer).onOutputStreamOffsetUsChanged(200); + inOrder.verify(renderer).onProcessedStreamChange(); + inOrder.verify(renderer).onOutputFormatChanged(eq(format3), any()); + inOrder.verify(renderer).onProcessedOutputBuffer(200); + inOrder.verify(renderer).onProcessedOutputBuffer(300); + inOrder.verify(renderer).onProcessedOutputBuffer(400); + } + private FakeSampleStream createFakeSampleStream(Format format, long... sampleTimesUs) { ImmutableList.Builder sampleListBuilder = ImmutableList.builder(); From 66b128251752e09bcd75f227278ed550d96951d9 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 28 Mar 2023 16:39:42 +0000 Subject: [PATCH 12/35] Post `OfflineLicenseHelper` interactions to its internal handler thread `DefaultDrmSession(Manager)` expect most of their methods to be called on the 'playback thread'. There isn't a playback thread in the case of `OfflineLicenseHelper`, but in that case it's the thread backing `DefaultDrmSessionManager.playbackLooper`, which is `OfflineLicenseHelper.handlerThread`. PiperOrigin-RevId: 520053006 (cherry picked from commit 376bddef4720e07d18afeff0c591f03bf72149f7) --- .../exoplayer/drm/OfflineLicenseHelper.java | 225 ++++++++++++++---- 1 file changed, 181 insertions(+), 44 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java index ab7d4745d1..1dff7d6728 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/OfflineLicenseHelper.java @@ -19,6 +19,7 @@ import android.media.MediaDrm; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; import android.util.Pair; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -31,8 +32,11 @@ import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DefaultDrmSessionManager.Mode; import androidx.media3.exoplayer.drm.DrmSession.DrmSessionException; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; +import com.google.common.util.concurrent.SettableFuture; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutionException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** Helper class to download, renew and release offline licenses. */ @RequiresApi(18) @@ -42,9 +46,10 @@ public final class OfflineLicenseHelper { private static final Format FORMAT_WITH_EMPTY_DRM_INIT_DATA = new Format.Builder().setDrmInitData(new DrmInitData()).build(); - private final ConditionVariable conditionVariable; + private final ConditionVariable drmListenerConditionVariable; private final DefaultDrmSessionManager drmSessionManager; private final HandlerThread handlerThread; + private final Handler handler; private final DrmSessionEventListener.EventDispatcher eventDispatcher; /** @@ -156,28 +161,29 @@ public final class OfflineLicenseHelper { this.eventDispatcher = eventDispatcher; handlerThread = new HandlerThread("ExoPlayer:OfflineLicenseHelper"); handlerThread.start(); - conditionVariable = new ConditionVariable(); + handler = new Handler(handlerThread.getLooper()); + drmListenerConditionVariable = new ConditionVariable(); DrmSessionEventListener eventListener = new DrmSessionEventListener() { @Override public void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { - conditionVariable.open(); + drmListenerConditionVariable.open(); } @Override public void onDrmSessionManagerError( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, Exception e) { - conditionVariable.open(); + drmListenerConditionVariable.open(); } @Override public void onDrmKeysRestored(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { - conditionVariable.open(); + drmListenerConditionVariable.open(); } @Override public void onDrmKeysRemoved(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { - conditionVariable.open(); + drmListenerConditionVariable.open(); } }; eventDispatcher.addEventListener(new Handler(handlerThread.getLooper()), eventListener); @@ -193,7 +199,8 @@ public final class OfflineLicenseHelper { */ public synchronized byte[] downloadLicense(Format format) throws DrmSessionException { Assertions.checkArgument(format.drmInitData != null); - return blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, format); + return acquireSessionAndGetOfflineLicenseKeySetIdOnHandlerThread( + DefaultDrmSessionManager.MODE_DOWNLOAD, /* offlineLicenseKeySetId= */ null, format); } /** @@ -206,7 +213,7 @@ public final class OfflineLicenseHelper { public synchronized byte[] renewLicense(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - return blockingKeyRequest( + return acquireSessionAndGetOfflineLicenseKeySetIdOnHandlerThread( DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, FORMAT_WITH_EMPTY_DRM_INIT_DATA); @@ -221,7 +228,7 @@ public final class OfflineLicenseHelper { public synchronized void releaseLicense(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - blockingKeyRequest( + acquireSessionAndGetOfflineLicenseKeySetIdOnHandlerThread( DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, FORMAT_WITH_EMPTY_DRM_INIT_DATA); @@ -237,25 +244,39 @@ public final class OfflineLicenseHelper { public synchronized Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); - drmSessionManager.setPlayer(handlerThread.getLooper(), PlayerId.UNSET); - drmSessionManager.prepare(); - DrmSession drmSession = - openBlockingKeyRequest( - DefaultDrmSessionManager.MODE_QUERY, - offlineLicenseKeySetId, - FORMAT_WITH_EMPTY_DRM_INIT_DATA); - DrmSessionException error = drmSession.getError(); - Pair licenseDurationRemainingSec = - WidevineUtil.getLicenseDurationRemainingSec(drmSession); - drmSession.release(eventDispatcher); - drmSessionManager.release(); - if (error != null) { - if (error.getCause() instanceof KeysExpiredException) { + DrmSession drmSession; + try { + drmSession = + acquireFirstSessionOnHandlerThread( + DefaultDrmSessionManager.MODE_QUERY, + offlineLicenseKeySetId, + FORMAT_WITH_EMPTY_DRM_INIT_DATA); + } catch (DrmSessionException e) { + if (e.getCause() instanceof KeysExpiredException) { return Pair.create(0L, 0L); } - throw error; + throw e; + } + + SettableFuture> licenseDurationRemainingSec = SettableFuture.create(); + handler.post( + () -> { + try { + licenseDurationRemainingSec.set( + Assertions.checkNotNull(WidevineUtil.getLicenseDurationRemainingSec(drmSession))); + } catch (Throwable e) { + licenseDurationRemainingSec.setException(e); + } finally { + drmSession.release(eventDispatcher); + } + }); + try { + return licenseDurationRemainingSec.get(); + } catch (ExecutionException | InterruptedException e) { + throw new IllegalStateException(e); + } finally { + releaseManagerOnHandlerThread(); } - return Assertions.checkNotNull(licenseDurationRemainingSec); } /** Releases the helper. Should be called when the helper is no longer required. */ @@ -263,30 +284,146 @@ public final class OfflineLicenseHelper { handlerThread.quit(); } - private byte[] blockingKeyRequest( + /** + * Returns the result of {@link DrmSession#getOfflineLicenseKeySetId()}, or throws {@link + * NullPointerException} if it's null. + * + *

This method takes care of acquiring and releasing the {@link DrmSessionManager} and {@link + * DrmSession} instances needed. + */ + private byte[] acquireSessionAndGetOfflineLicenseKeySetIdOnHandlerThread( @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format) throws DrmSessionException { - drmSessionManager.setPlayer(handlerThread.getLooper(), PlayerId.UNSET); - drmSessionManager.prepare(); - DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, format); - DrmSessionException error = drmSession.getError(); - byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); - drmSession.release(eventDispatcher); - drmSessionManager.release(); - if (error != null) { - throw error; + DrmSession drmSession = + acquireFirstSessionOnHandlerThread(licenseMode, offlineLicenseKeySetId, format); + + SettableFuture keySetId = SettableFuture.create(); + handler.post( + () -> { + try { + keySetId.set(drmSession.getOfflineLicenseKeySetId()); + } catch (Throwable e) { + keySetId.setException(e); + } finally { + drmSession.release(eventDispatcher); + } + }); + + try { + return Assertions.checkNotNull(keySetId.get()); + } catch (ExecutionException | InterruptedException e) { + throw new IllegalStateException(e); + } finally { + releaseManagerOnHandlerThread(); } - return Assertions.checkNotNull(keySetId); } - private DrmSession openBlockingKeyRequest( - @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format) { + /** + * Calls {@link DrmSessionManager#acquireSession(DrmSessionEventListener.EventDispatcher, Format)} + * on {@link #handlerThread} and blocks until a callback is received via {@link + * DrmSessionEventListener}. + * + *

If key loading failed and {@link DrmSession#getState()} returns {@link + * DrmSession#STATE_ERROR} then this method releases the session and throws {@link + * DrmSession#getError()}. + * + *

Callers are responsible for the following: + * + *

    + *
  • Ensuring the {@link + * DrmSessionManager#acquireSession(DrmSessionEventListener.EventDispatcher, Format)} call + * will trigger a callback to {@link DrmSessionEventListener} (e.g. it will load new keys). + * If not, this method will block forever. + *
  • Releasing the returned {@link DrmSession} instance (on {@link #handlerThread}). + *
  • Releasing {@link #drmSessionManager} if a {@link DrmSession} instance is returned (the + * manager will be released before an exception is thrown). + *
+ */ + private DrmSession acquireFirstSessionOnHandlerThread( + @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, Format format) + throws DrmSessionException { Assertions.checkNotNull(format.drmInitData); - drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); - conditionVariable.close(); - DrmSession drmSession = drmSessionManager.acquireSession(eventDispatcher, format); - // Block current thread until key loading is finished - conditionVariable.block(); - return Assertions.checkNotNull(drmSession); + SettableFuture drmSessionFuture = SettableFuture.create(); + drmListenerConditionVariable.close(); + handler.post( + () -> { + try { + drmSessionManager.setPlayer(Assertions.checkNotNull(Looper.myLooper()), PlayerId.UNSET); + drmSessionManager.prepare(); + try { + drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); + drmSessionFuture.set( + Assertions.checkNotNull( + drmSessionManager.acquireSession(eventDispatcher, format))); + } catch (Throwable e) { + drmSessionManager.release(); + throw e; + } + } catch (Throwable e) { + drmSessionFuture.setException(e); + } + }); + + DrmSession drmSession; + try { + drmSession = drmSessionFuture.get(); + } catch (ExecutionException | InterruptedException e) { + throw new IllegalStateException(e); + } + + // drmListenerConditionVariable will be opened by a callback to this.eventDispatcher when key + // loading is complete (drmSession.state == STATE_OPENED_WITH_KEYS) or has failed + // (drmSession.state == STATE_ERROR). + drmListenerConditionVariable.block(); + + SettableFuture<@NullableType DrmSessionException> drmSessionErrorFuture = + SettableFuture.create(); + handler.post( + () -> { + try { + DrmSessionException drmSessionError = drmSession.getError(); + if (drmSession.getState() == DrmSession.STATE_ERROR) { + drmSession.release(eventDispatcher); + drmSessionManager.release(); + } + drmSessionErrorFuture.set(drmSessionError); + } catch (Throwable e) { + drmSessionErrorFuture.setException(e); + drmSession.release(eventDispatcher); + drmSessionManager.release(); + } + }); + try { + DrmSessionException drmSessionError = drmSessionErrorFuture.get(); + if (drmSessionError != null) { + throw drmSessionError; + } else { + return drmSession; + } + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + } + + /** + * Calls {@link DrmSessionManager#release()} on {@link #handlerThread} and blocks until it's + * complete. + */ + private void releaseManagerOnHandlerThread() { + SettableFuture result = SettableFuture.create(); + handler.post( + () -> { + try { + drmSessionManager.release(); + result.set(null); + } catch (Throwable e) { + result.setException(e); + } + }); + try { + result.get(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } } } From d07fcc34570c439f7fdb16f4da5019db0a4d2c53 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 29 Mar 2023 13:09:27 +0000 Subject: [PATCH 13/35] Remove remaining references to exoplayer.dev PiperOrigin-RevId: 520314781 (cherry picked from commit 6952771e2f0637e91e27ed8851dc5b2a5a199afe) --- .github/ISSUE_TEMPLATE/question.md | 2 +- demos/main/src/main/res/values/strings.xml | 2 +- demos/transformer/README.md | 2 +- .../src/main/java/androidx/media3/common/Format.java | 3 ++- .../java/androidx/media3/common/PlaybackException.java | 5 +++-- .../java/androidx/media3/common/SimpleBasePlayer.java | 3 ++- .../androidx/media3/datasource/HttpDataSource.java | 6 +++--- libraries/decoder_ffmpeg/README.md | 2 +- .../java/androidx/media3/exoplayer/ExoPlayerImpl.java | 3 ++- .../exoplayer/source/DefaultMediaSourceFactory.java | 10 +++++----- libraries/exoplayer_ima/README.md | 2 +- 11 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 725b774911..386abe6bf9 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -36,7 +36,7 @@ In case your question is related to a piece of media: - Authentication HTTP headers Don't forget to check ExoPlayer's supported formats and devices, if applicable -(https://exoplayer.dev/supported-formats.html). +(https://developer.android.com/guide/topics/media/exoplayer/supported-formats). If there's something you don't want to post publicly, please submit the issue, then email the link/bug report to dev.exoplayer@gmail.com using a subject in the diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index ce9c90d0c2..b8f6e7c320 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 HTTP traffic not permitted. See https://exoplayer.dev/issues/cleartext-not-permitted + Cleartext HTTP traffic not permitted. See https://developer.android.com/guide/topics/media/issues/cleartext-not-permitted Playback failed diff --git a/demos/transformer/README.md b/demos/transformer/README.md index fd767ba6c8..9733337292 100644 --- a/demos/transformer/README.md +++ b/demos/transformer/README.md @@ -61,6 +61,6 @@ manual steps. (this will only appear if the AAR is present), then build and run the demo app and select a MediaPipe-based effect. -[Transformer]: https://exoplayer.dev/transforming-media.html +[Transformer]: https://developer.android.com/guide/topics/media/transforming-media [MediaPipe]: https://google.github.io/mediapipe/ [build an AAR]: https://google.github.io/mediapipe/getting_started/android_archive_library.html diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java index b34bead66e..37dd95e79b 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Format.java +++ b/libraries/common/src/main/java/androidx/media3/common/Format.java @@ -35,7 +35,8 @@ import java.util.UUID; * *

When building formats, populate all fields whose values are known and relevant to the type of * format being constructed. For information about different types of format, see ExoPlayer's Supported formats page. + * href="https://developer.android.com/guide/topics/media/exoplayer/supported-formats">Supported + * formats page. * *

Fields commonly relevant to all formats

* diff --git a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java index f9aa597856..d0b93bd157 100644 --- a/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java +++ b/libraries/common/src/main/java/androidx/media3/common/PlaybackException.java @@ -152,8 +152,9 @@ public class PlaybackException extends Exception implements Bundleable { * Caused by the player trying to access cleartext HTTP traffic (meaning http:// rather than * https://) when the app's Network Security Configuration does not permit it. * - *

See this corresponding - * troubleshooting topic. + *

See this + * corresponding troubleshooting topic. */ public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007; /** Caused by reading data out of the data bound. */ diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 842f63912b..22757d66bd 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -3355,7 +3355,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { "Player is accessed on the wrong thread.\n" + "Current thread: '%s'\n" + "Expected thread: '%s'\n" - + "See https://exoplayer.dev/issues/player-accessed-on-wrong-thread", + + "See https://developer.android.com/guide/topics/media/issues/" + + "player-accessed-on-wrong-thread", Thread.currentThread().getName(), applicationLooper.getThread().getName()); throw new IllegalStateException(message); } diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java index cf17e7c070..e83e21b4fd 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java @@ -375,8 +375,8 @@ public interface HttpDataSource extends DataSource { /** * Thrown when cleartext HTTP traffic is not permitted. For more information including how to * enable cleartext traffic, see the corresponding troubleshooting - * topic. + * href="https://developer.android.com/guide/topics/media/issues/cleartext-not-permitted">corresponding + * troubleshooting topic. */ final class CleartextNotPermittedException extends HttpDataSourceException { @@ -384,7 +384,7 @@ public interface HttpDataSource extends DataSource { public CleartextNotPermittedException(IOException cause, DataSpec dataSpec) { super( "Cleartext HTTP traffic not permitted. See" - + " https://exoplayer.dev/issues/cleartext-not-permitted", + + " https://developer.android.com/guide/topics/media/issues/cleartext-not-permitted", cause, dataSpec, PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, diff --git a/libraries/decoder_ffmpeg/README.md b/libraries/decoder_ffmpeg/README.md index 204dd1bba6..7e7bd80f3a 100644 --- a/libraries/decoder_ffmpeg/README.md +++ b/libraries/decoder_ffmpeg/README.md @@ -115,7 +115,7 @@ then implement your own logic to use the renderer for a given track. [top level README]: ../../README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [ExoPlayer issue 2781]: https://github.com/google/ExoPlayer/issues/2781 -[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension +[Supported formats]: https://developer.android.com/guide/topics/media/exoplayer/supported-formats#ffmpeg-library ## Links diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 10c64a7e98..1a830f5c5e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -2673,7 +2673,8 @@ import java.util.concurrent.TimeoutException; "Player is accessed on the wrong thread.\n" + "Current thread: '%s'\n" + "Expected thread: '%s'\n" - + "See https://exoplayer.dev/issues/player-accessed-on-wrong-thread", + + "See https://developer.android.com/guide/topics/media/issues/" + + "player-accessed-on-wrong-thread", Thread.currentThread().getName(), getApplicationLooper().getThread().getName()); if (throwsWhenUsingWrongThread) { throw new IllegalStateException(message); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java index 64f849448d..4fd667eb26 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java @@ -71,17 +71,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; *

  • {@code DashMediaSource.Factory} if the item's {@link MediaItem.LocalConfiguration#uri uri} * ends in '.mpd' or if its {@link MediaItem.LocalConfiguration#mimeType mimeType field} is * explicitly set to {@link MimeTypes#APPLICATION_MPD} (Requires the exoplayer-dash module - * to be added to the app). + * href="https://developer.android.com/guide/topics/media/exoplayer/hello-world#add-exoplayer-modules">exoplayer-dash + * module to be added to the app). *
  • {@code HlsMediaSource.Factory} if the item's {@link MediaItem.LocalConfiguration#uri uri} * ends in '.m3u8' or if its {@link MediaItem.LocalConfiguration#mimeType mimeType field} is * explicitly set to {@link MimeTypes#APPLICATION_M3U8} (Requires the exoplayer-hls module to - * be added to the app). + * href="https://developer.android.com/guide/topics/media/exoplayer/hello-world#add-exoplayer-modules">exoplayer-hls + * module to be added to the app). *
  • {@code SsMediaSource.Factory} if the item's {@link MediaItem.LocalConfiguration#uri uri} * ends in '.ism', '.ism/Manifest' or if its {@link MediaItem.LocalConfiguration#mimeType * mimeType field} is explicitly set to {@link MimeTypes#APPLICATION_SS} (Requires the + * href="https://developer.android.com/guide/topics/media/exoplayer/hello-world#add-exoplayer-modules"> * exoplayer-smoothstreaming module to be added to the app). *
  • {@link ProgressiveMediaSource.Factory} serves as a fallback if the item's {@link * MediaItem.LocalConfiguration#uri uri} doesn't match one of the above. It tries to infer the diff --git a/libraries/exoplayer_ima/README.md b/libraries/exoplayer_ima/README.md index f7d9750339..a264cde5cf 100644 --- a/libraries/exoplayer_ima/README.md +++ b/libraries/exoplayer_ima/README.md @@ -26,7 +26,7 @@ locally. Instructions for doing this can be found in the [top level README][]. ## Using the module To use the module, follow the instructions on the -[Ad insertion page](https://exoplayer.dev/ad-insertion.html#declarative-ad-support) +[Ad insertion page](https://developer.android.com/guide/topics/media/exoplayer/ad-insertion#declarative-ad-support) of the developer guide. The `AdsLoaderProvider` passed to the player's `DefaultMediaSourceFactory` should return an `ImaAdsLoader`. Note that the IMA module only supports players that are accessed on the application's main thread. From 10342507f7c2e167e95365609ea584d227bc9b5d Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 30 Mar 2023 15:35:23 +0000 Subject: [PATCH 14/35] Temporarily use exoplayer2-hosted SVGs in media3 javadoc The media3-hosted versions of these SVGs were removed due to a change in the way the reference docs are generated. While work on getting them hosted on developer.android.com, this change simply links to the (identical) exoplayer2 versions in order to fix the media3 docs. #minor-release PiperOrigin-RevId: 520647905 (cherry picked from commit 2e4f49fef741c21f47590d3a818b6ed43d94fc04) --- .../java/androidx/media3/common/Timeline.java | 37 ++++++++++++------- .../androidx/media3/exoplayer/ExoPlayer.java | 7 +++- .../androidx/media3/exoplayer/Renderer.java | 7 +++- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java index 81459888a1..4fd2474dcd 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -61,8 +61,9 @@ import java.util.List; * *

    Single media file or on-demand stream

    * - *

    Example timeline for a
- * single file + *

    Example timeline for a single file * *

    A timeline for a single media file or on-demand stream consists of a single period and window. * The window spans the whole period, indicating that all parts of the media are available for @@ -71,8 +72,9 @@ import java.util.List; * *

    Playlist of media files or on-demand streams

    * - *

    Example timeline for a
- * playlist of files + *

    Example timeline for a playlist of files * *

    A timeline for a playlist of media files or on-demand streams consists of multiple periods, * each with its own window. Each window spans the whole of the corresponding period, and typically @@ -82,8 +84,9 @@ import java.util.List; * *

    Live stream with limited availability

    * - *

    Example timeline for
- * a live stream with limited availability + *

    Example timeline for a live stream with limited availability * *

    A timeline for a live stream consists of a period whose duration is unknown, since it's * continually extending as more content is broadcast. If content only remains available for a @@ -95,8 +98,9 @@ import java.util.List; * *

    Live stream with indefinite availability

    * - *

    Example timeline
- * for a live stream with indefinite availability + *

    Example timeline for a live stream with indefinite availability * *

    A timeline for a live stream with indefinite availability is similar to the Live stream with limited availability case, except that the window @@ -105,8 +109,9 @@ import java.util.List; * *

    Live stream with multiple periods

    * - *

    Example timeline
- * for a live stream with multiple periods + *

    Example timeline for a live stream with multiple periods * *

    This case arises when a live stream is explicitly divided into separate periods, for example * at content boundaries. This case is similar to the Live stream with @@ -115,8 +120,9 @@ import java.util.List; * *

    On-demand stream followed by live stream

    * - *

    Example timeline for an
- * on-demand stream followed by a live stream + *

    Example timeline for an on-demand stream followed by a live stream * *

    This case is the concatenation of the Single media file or on-demand * stream and Live stream with multiple periods cases. When playback @@ -125,12 +131,15 @@ import java.util.List; * *

    On-demand stream with mid-roll ads

    * - *

    Example
- * timeline for an on-demand stream with mid-roll ad groups + *

    Example timeline for an on-demand stream with mid-roll ad groups * *

    This case includes mid-roll ad groups, which are defined as part of the timeline's single * period. The period can be queried for information about the ad groups and the ads they contain. */ +// TODO(b/276289331): Revert to media3-hosted SVG links above once they're available on +// developer.android.com. public abstract class Timeline implements Bundleable { /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index e58db58847..74e858437d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -128,8 +128,9 @@ import java.util.List; * *

    The figure below shows ExoPlayer's threading model. * - *

    ExoPlayer's
- * threading model + *

    ExoPlayer's threading model * *

      *
    • ExoPlayer instances must be accessed from a single application thread unless indicated @@ -158,6 +159,8 @@ import java.util.List; * may use background threads to load data. These are implementation specific. *
    */ +// TODO(b/276289331): Revert to media3-hosted SVG links above once they're available on +// developer.android.com. public interface ExoPlayer extends Player { /** diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java index 22274d0860..5dee3f3298 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -47,9 +47,12 @@ import java.lang.annotation.Target; * valid state transitions are shown below, annotated with the methods that are called during each * transition. * - *

    Renderer state
- * transitions + *

    Renderer state transitions */ +// TODO(b/276289331): Revert to media3-hosted SVG links above once they're available on +// developer.android.com. @UnstableApi public interface Renderer extends PlayerMessage.Target { From 3daaad7acdbc5859f3ca9cc02d73d41f90dd4c86 Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 30 Mar 2023 20:14:50 +0100 Subject: [PATCH 15/35] ...Suppress `MissingSuperCall` warnings on onBackPressed()... PiperOrigin-RevId: 520709134 (cherry picked from commit 5af28a980d657cf1038be34f1d7939104e5e721b) --- .../src/main/java/androidx/media3/demo/session/MainActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt index 810a6ac9b7..e012125cb9 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt @@ -83,6 +83,7 @@ class MainActivity : AppCompatActivity() { return super.onOptionsItemSelected(item) } + @SuppressWarnings("MissingSuperCall") override fun onBackPressed() { popPathStack() } From 3480a27994ef9e06bd7574bad4656eb8c7677971 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 31 Mar 2023 09:56:59 +0100 Subject: [PATCH 16/35] Add warning logs if `DefaultDrmSessionManager` is used on wrong thread Issue: google/ExoPlayer#11008 PiperOrigin-RevId: 520864579 (cherry picked from commit 7ca966842185dd6f07563f28058d78537b916731) --- .../exoplayer/drm/DefaultDrmSession.java | 31 +++++++++++++++++-- .../drm/DefaultDrmSessionManager.java | 22 +++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java index c64eb57dab..b04c5b1cd8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSession.java @@ -136,9 +136,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final PlayerId playerId; - /* package */ final MediaDrmCallback callback; - /* package */ final UUID uuid; - /* package */ final ResponseHandler responseHandler; + private final MediaDrmCallback callback; + private final UUID uuid; + private final Looper playbackLooper; + private final ResponseHandler responseHandler; private @DrmSession.State int state; private int referenceCount; @@ -209,10 +210,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.playerId = playerId; state = STATE_OPENING; + this.playbackLooper = playbackLooper; responseHandler = new ResponseHandler(playbackLooper); } public boolean hasSessionId(byte[] sessionId) { + verifyPlaybackThread(); return Arrays.equals(this.sessionId, sessionId); } @@ -255,50 +258,59 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public final @DrmSession.State int getState() { + verifyPlaybackThread(); return state; } @Override public boolean playClearSamplesWithoutKeys() { + verifyPlaybackThread(); return playClearSamplesWithoutKeys; } @Override @Nullable public final DrmSessionException getError() { + verifyPlaybackThread(); return state == STATE_ERROR ? lastException : null; } @Override public final UUID getSchemeUuid() { + verifyPlaybackThread(); return uuid; } @Override @Nullable public final CryptoConfig getCryptoConfig() { + verifyPlaybackThread(); return cryptoConfig; } @Override @Nullable public Map queryKeyStatus() { + verifyPlaybackThread(); return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); } @Override @Nullable public byte[] getOfflineLicenseKeySetId() { + verifyPlaybackThread(); return offlineLicenseKeySetId; } @Override public boolean requiresSecureDecoder(String mimeType) { + verifyPlaybackThread(); return mediaDrm.requiresSecureDecoder(checkStateNotNull(sessionId), mimeType); } @Override public void acquire(@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + verifyPlaybackThread(); if (referenceCount < 0) { Log.e(TAG, "Session reference count less than zero: " + referenceCount); referenceCount = 0; @@ -326,6 +338,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @Override public void release(@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher) { + verifyPlaybackThread(); if (referenceCount <= 0) { Log.e(TAG, "release() called on a session that's already fully released."); return; @@ -561,6 +574,18 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } + private void verifyPlaybackThread() { + if (Thread.currentThread() != playbackLooper.getThread()) { + Log.w( + TAG, + "DefaultDrmSession accessed on the wrong thread.\nCurrent thread: " + + Thread.currentThread().getName() + + "\nExpected thread: " + + playbackLooper.getThread().getName(), + new IllegalStateException()); + } + } + // Internal classes. @SuppressLint("HandlerLeak") diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java index 9a345ffff7..05ab52a039 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java @@ -471,6 +471,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Override public final void prepare() { + verifyPlaybackThread(/* allowBeforeSetPlayer= */ true); if (prepareCallsCount++ != 0) { return; } @@ -487,6 +488,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Override public final void release() { + verifyPlaybackThread(/* allowBeforeSetPlayer= */ true); if (--prepareCallsCount != 0) { return; } @@ -513,6 +515,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Override public DrmSessionReference preacquireSession( @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) { + // Don't verify the playback thread, preacquireSession can be called from any thread. checkState(prepareCallsCount > 0); checkStateNotNull(playbackLooper); PreacquiredSessionReference preacquiredSessionReference = @@ -525,6 +528,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Nullable public DrmSession acquireSession( @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, Format format) { + verifyPlaybackThread(/* allowBeforeSetPlayer= */ false); checkState(prepareCallsCount > 0); checkStateNotNull(playbackLooper); return acquireSession( @@ -599,6 +603,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { @Override public @C.CryptoType int getCryptoType(Format format) { + verifyPlaybackThread(/* allowBeforeSetPlayer= */ false); @C.CryptoType int cryptoType = checkNotNull(exoMediaDrm).getCryptoType(); if (format.drmInitData == null) { int trackType = MimeTypes.getTrackType(format.sampleMimeType); @@ -817,6 +822,23 @@ public class DefaultDrmSessionManager implements DrmSessionManager { } } + private void verifyPlaybackThread(boolean allowBeforeSetPlayer) { + if (allowBeforeSetPlayer && playbackLooper == null) { + Log.w( + TAG, + "DefaultDrmSessionManager accessed before setPlayer(), possibly on the wrong thread.", + new IllegalStateException()); + } else if (Thread.currentThread() != checkNotNull(playbackLooper).getThread()) { + Log.w( + TAG, + "DefaultDrmSessionManager accessed on the wrong thread.\nCurrent thread: " + + Thread.currentThread().getName() + + "\nExpected thread: " + + playbackLooper.getThread().getName(), + new IllegalStateException()); + } + } + /** * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}. * From 654a6786a6fa1c2e7f1c3bc1342522094365f005 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 3 Apr 2023 11:45:15 +0100 Subject: [PATCH 17/35] Improve check for invalid connection request The check currently relies on the default value of 0 returned if the Bundle doesn't define a pid. But in some cases, like Robolectric unit tests, 0 is a possible pid. The check can be improved by directly asserting that the value is defined. PiperOrigin-RevId: 521414649 (cherry picked from commit 694d690bc03a34e112afc6cca58eb2761a25402a) --- .../main/java/androidx/media3/session/ConnectionRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java index 037baff628..b9f6258594 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionRequest.java @@ -89,8 +89,8 @@ import androidx.media3.common.util.Util; int controllerInterfaceVersion = bundle.getInt(FIELD_CONTROLLER_INTERFACE_VERSION, /* defaultValue= */ 0); String packageName = checkNotNull(bundle.getString(FIELD_PACKAGE_NAME)); - int pid = bundle.getInt(FIELD_PID, /* defaultValue= */ 0); - checkArgument(pid != 0); + checkArgument(bundle.containsKey(FIELD_PID)); + int pid = bundle.getInt(FIELD_PID); @Nullable Bundle connectionHints = bundle.getBundle(FIELD_CONNECTION_HINTS); return new ConnectionRequest( libraryVersion, From 10e2dfd2fac5da24f285afd1eca5c3bafd4ff689 Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 5 Apr 2023 10:26:04 +0100 Subject: [PATCH 18/35] Remove Javadoc TODO from README of decoder extensions As most classes are used via interface only and people depending on it locally can always find the Javadoc in Android Studio directly, we don't plan to add Javadocs for these extensions module in developer.android.com. PiperOrigin-RevId: 521993756 (cherry picked from commit e4cb583af2feb607ec5c2c201c22cbd661e454a8) --- libraries/decoder_av1/README.md | 2 -- libraries/decoder_ffmpeg/README.md | 2 -- libraries/decoder_flac/README.md | 2 -- libraries/decoder_opus/README.md | 2 -- libraries/decoder_vp9/README.md | 2 -- 5 files changed, 10 deletions(-) diff --git a/libraries/decoder_av1/README.md b/libraries/decoder_av1/README.md index bdf5083097..c85350595b 100644 --- a/libraries/decoder_av1/README.md +++ b/libraries/decoder_av1/README.md @@ -128,6 +128,4 @@ GL rendering mode has better performance, so should be preferred * [Troubleshooting using decoding extensions][] - - [Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playbacks diff --git a/libraries/decoder_ffmpeg/README.md b/libraries/decoder_ffmpeg/README.md index 7e7bd80f3a..5adf97bdcd 100644 --- a/libraries/decoder_ffmpeg/README.md +++ b/libraries/decoder_ffmpeg/README.md @@ -121,6 +121,4 @@ then implement your own logic to use the renderer for a given track. * [Troubleshooting using decoding extensions][] - - [Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/decoder_flac/README.md b/libraries/decoder_flac/README.md index 3599493410..9db717585a 100644 --- a/libraries/decoder_flac/README.md +++ b/libraries/decoder_flac/README.md @@ -100,6 +100,4 @@ player, then implement your own logic to use the renderer for a given track. * [Troubleshooting using decoding extensions][] - - [Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/decoder_opus/README.md b/libraries/decoder_opus/README.md index e879f1a87a..dcbfd5b77d 100644 --- a/libraries/decoder_opus/README.md +++ b/libraries/decoder_opus/README.md @@ -104,6 +104,4 @@ player, then implement your own logic to use the renderer for a given track. * [Troubleshooting using decoding extensions][] - - [Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback diff --git a/libraries/decoder_vp9/README.md b/libraries/decoder_vp9/README.md index 633d00a794..1995fe55fd 100644 --- a/libraries/decoder_vp9/README.md +++ b/libraries/decoder_vp9/README.md @@ -141,6 +141,4 @@ GL rendering mode has better performance, so should be preferred. * [Troubleshooting using decoding extensions][] - - [Troubleshooting using decoding extensions]: https://developer.android.com/guide/topics/media/exoplayer/troubleshooting#how-can-i-get-a-decoding-library-to-load-and-be-used-for-playback From 86cc91687cb11fa763ea283908c7fb10c646d0c6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 5 Apr 2023 10:26:31 +0100 Subject: [PATCH 19/35] Allow duplicated MediaItems in a legacy session MediaItems are not meant to be unique in a playlist. If a legacy session publishes multiple items that get converted to equal MediaItems, the current code fails because we look up queue ids in a Map (that doesn't allow duplicate entries). Fix this by storing a simple list of items with additional data. Issue: androidx/media#290 PiperOrigin-RevId: 521993802 (cherry picked from commit 219967c5a3ff8d8957a07e39c04ea27b657cab92) --- .../session/MediaControllerImplLegacy.java | 12 +- .../media3/session/QueueTimeline.java | 166 +++++++++--------- ...aControllerWithMediaSessionCompatTest.java | 36 ++++ 3 files changed, 128 insertions(+), 86 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index cd0a0a1fca..e01ffcbf49 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -1795,7 +1795,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; currentTimeline = isQueueChanged ? QueueTimeline.create(newLegacyPlayerInfo.queue) - : new QueueTimeline((QueueTimeline) oldControllerInfo.playerInfo.timeline); + : ((QueueTimeline) oldControllerInfo.playerInfo.timeline).copy(); boolean isMetadataCompatChanged = oldLegacyPlayerInfo.mediaMetadataCompat != newLegacyPlayerInfo.mediaMetadataCompat @@ -1987,8 +1987,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Integer mediaItemTransitionReason; boolean isOldTimelineEmpty = oldControllerInfo.playerInfo.timeline.isEmpty(); boolean isNewTimelineEmpty = newControllerInfo.playerInfo.timeline.isEmpty(); - int newCurrentMediaItemIndex = - newControllerInfo.playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex; if (isOldTimelineEmpty && isNewTimelineEmpty) { // Still empty Timelines. discontinuityReason = null; @@ -2000,13 +1998,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else { MediaItem oldCurrentMediaItem = checkStateNotNull(oldControllerInfo.playerInfo.getCurrentMediaItem()); - int oldCurrentMediaItemIndexInNewTimeline = - ((QueueTimeline) newControllerInfo.playerInfo.timeline).indexOf(oldCurrentMediaItem); - if (oldCurrentMediaItemIndexInNewTimeline == C.INDEX_UNSET) { + boolean oldCurrentMediaItemExistsInNewTimeline = + ((QueueTimeline) newControllerInfo.playerInfo.timeline).contains(oldCurrentMediaItem); + if (!oldCurrentMediaItemExistsInNewTimeline) { // Old current item is removed. discontinuityReason = Player.DISCONTINUITY_REASON_REMOVE; mediaItemTransitionReason = Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED; - } else if (oldCurrentMediaItemIndexInNewTimeline == newCurrentMediaItemIndex) { + } else if (oldCurrentMediaItem.equals(newControllerInfo.playerInfo.getCurrentMediaItem())) { // Current item is the same. long oldCurrentPosition = MediaUtils.convertToCurrentPositionMs( diff --git a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java index a7dc94c511..2901789217 100644 --- a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java +++ b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java @@ -16,7 +16,6 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkArgument; -import static androidx.media3.common.util.Assertions.checkNotNull; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat.QueueItem; @@ -27,11 +26,8 @@ import androidx.media3.common.Timeline; import androidx.media3.common.util.Util; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * An immutable class to represent the current {@link Timeline} backed by {@linkplain QueueItem @@ -45,42 +41,33 @@ import java.util.Map; /* package */ final class QueueTimeline extends Timeline { public static final QueueTimeline DEFAULT = - new QueueTimeline(ImmutableList.of(), ImmutableMap.of(), /* fakeMediaItem= */ null); + new QueueTimeline(ImmutableList.of(), /* fakeMediaItem= */ null); private static final Object FAKE_WINDOW_UID = new Object(); - private final ImmutableList mediaItems; - private final ImmutableMap mediaItemToQueueIdMap; + private final ImmutableList queuedMediaItems; @Nullable private final MediaItem fakeMediaItem; - /** Creates a new instance. */ - public QueueTimeline(QueueTimeline queueTimeline) { - this.mediaItems = queueTimeline.mediaItems; - this.mediaItemToQueueIdMap = queueTimeline.mediaItemToQueueIdMap; - this.fakeMediaItem = queueTimeline.fakeMediaItem; - } - private QueueTimeline( - ImmutableList mediaItems, - ImmutableMap mediaItemToQueueIdMap, - @Nullable MediaItem fakeMediaItem) { - this.mediaItems = mediaItems; - this.mediaItemToQueueIdMap = mediaItemToQueueIdMap; + ImmutableList queuedMediaItems, @Nullable MediaItem fakeMediaItem) { + this.queuedMediaItems = queuedMediaItems; this.fakeMediaItem = fakeMediaItem; } /** Creates a {@link QueueTimeline} from a list of {@linkplain QueueItem queue items}. */ public static QueueTimeline create(List queue) { - ImmutableList.Builder mediaItemsBuilder = new ImmutableList.Builder<>(); - ImmutableMap.Builder mediaItemToQueueIdMap = new ImmutableMap.Builder<>(); + ImmutableList.Builder queuedMediaItemsBuilder = new ImmutableList.Builder<>(); for (int i = 0; i < queue.size(); i++) { QueueItem queueItem = queue.get(i); MediaItem mediaItem = MediaUtils.convertToMediaItem(queueItem); - mediaItemsBuilder.add(mediaItem); - mediaItemToQueueIdMap.put(mediaItem, queueItem.getQueueId()); + queuedMediaItemsBuilder.add(new QueuedMediaItem(mediaItem, queueItem.getQueueId())); } - return new QueueTimeline( - mediaItemsBuilder.build(), mediaItemToQueueIdMap.buildOrThrow(), /* fakeMediaItem= */ null); + return new QueueTimeline(queuedMediaItemsBuilder.build(), /* fakeMediaItem= */ null); + } + + /** Returns a copy of the current queue timeline. */ + public QueueTimeline copy() { + return new QueueTimeline(queuedMediaItems, fakeMediaItem); } /** @@ -91,9 +78,9 @@ import java.util.Map; * @return The corresponding queue ID or {@link QueueItem#UNKNOWN_ID} if not known. */ public long getQueueId(int mediaItemIndex) { - MediaItem mediaItem = getMediaItemAt(mediaItemIndex); - @Nullable Long queueId = mediaItemToQueueIdMap.get(mediaItem); - return queueId == null ? QueueItem.UNKNOWN_ID : queueId; + return mediaItemIndex >= 0 && mediaItemIndex < queuedMediaItems.size() + ? queuedMediaItems.get(mediaItemIndex).queueId + : QueueItem.UNKNOWN_ID; } /** @@ -103,7 +90,7 @@ import java.util.Map; * @return A new {@link QueueTimeline} reflecting the update. */ public QueueTimeline copyWithFakeMediaItem(@Nullable MediaItem fakeMediaItem) { - return new QueueTimeline(mediaItems, mediaItemToQueueIdMap, fakeMediaItem); + return new QueueTimeline(queuedMediaItems, fakeMediaItem); } /** @@ -115,23 +102,17 @@ import java.util.Map; */ public QueueTimeline copyWithNewMediaItem(int replaceIndex, MediaItem newMediaItem) { checkArgument( - replaceIndex < mediaItems.size() - || (replaceIndex == mediaItems.size() && fakeMediaItem != null)); - if (replaceIndex == mediaItems.size()) { - return new QueueTimeline(mediaItems, mediaItemToQueueIdMap, newMediaItem); + replaceIndex < queuedMediaItems.size() + || (replaceIndex == queuedMediaItems.size() && fakeMediaItem != null)); + if (replaceIndex == queuedMediaItems.size()) { + return new QueueTimeline(queuedMediaItems, newMediaItem); } - MediaItem oldMediaItem = mediaItems.get(replaceIndex); - // Create the new play list. - ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); - newMediaItemsBuilder.addAll(mediaItems.subList(0, replaceIndex)); - newMediaItemsBuilder.add(newMediaItem); - newMediaItemsBuilder.addAll(mediaItems.subList(replaceIndex + 1, mediaItems.size())); - // Update the map of items to queue IDs accordingly. - Map newMediaItemToQueueIdMap = new HashMap<>(mediaItemToQueueIdMap); - Long queueId = checkNotNull(newMediaItemToQueueIdMap.remove(oldMediaItem)); - newMediaItemToQueueIdMap.put(newMediaItem, queueId); - return new QueueTimeline( - newMediaItemsBuilder.build(), ImmutableMap.copyOf(newMediaItemToQueueIdMap), fakeMediaItem); + long queueId = queuedMediaItems.get(replaceIndex).queueId; + ImmutableList.Builder queuedItemsBuilder = new ImmutableList.Builder<>(); + queuedItemsBuilder.addAll(queuedMediaItems.subList(0, replaceIndex)); + queuedItemsBuilder.add(new QueuedMediaItem(newMediaItem, queueId)); + queuedItemsBuilder.addAll(queuedMediaItems.subList(replaceIndex + 1, queuedMediaItems.size())); + return new QueueTimeline(queuedItemsBuilder.build(), fakeMediaItem); } /** @@ -143,11 +124,13 @@ import java.util.Map; * @return A new {@link QueueTimeline} reflecting the update. */ public QueueTimeline copyWithNewMediaItems(int index, List newMediaItems) { - ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); - newMediaItemsBuilder.addAll(mediaItems.subList(0, index)); - newMediaItemsBuilder.addAll(newMediaItems); - newMediaItemsBuilder.addAll(mediaItems.subList(index, mediaItems.size())); - return new QueueTimeline(newMediaItemsBuilder.build(), mediaItemToQueueIdMap, fakeMediaItem); + ImmutableList.Builder queuedItemsBuilder = new ImmutableList.Builder<>(); + queuedItemsBuilder.addAll(queuedMediaItems.subList(0, index)); + for (int i = 0; i < newMediaItems.size(); i++) { + queuedItemsBuilder.add(new QueuedMediaItem(newMediaItems.get(i), QueueItem.UNKNOWN_ID)); + } + queuedItemsBuilder.addAll(queuedMediaItems.subList(index, queuedMediaItems.size())); + return new QueueTimeline(queuedItemsBuilder.build(), fakeMediaItem); } /** @@ -158,10 +141,10 @@ import java.util.Map; * @return A new {@link QueueTimeline} reflecting the update. */ public QueueTimeline copyWithRemovedMediaItems(int fromIndex, int toIndex) { - ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); - newMediaItemsBuilder.addAll(mediaItems.subList(0, fromIndex)); - newMediaItemsBuilder.addAll(mediaItems.subList(toIndex, mediaItems.size())); - return new QueueTimeline(newMediaItemsBuilder.build(), mediaItemToQueueIdMap, fakeMediaItem); + ImmutableList.Builder queuedItemsBuilder = new ImmutableList.Builder<>(); + queuedItemsBuilder.addAll(queuedMediaItems.subList(0, fromIndex)); + queuedItemsBuilder.addAll(queuedMediaItems.subList(toIndex, queuedMediaItems.size())); + return new QueueTimeline(queuedItemsBuilder.build(), fakeMediaItem); } /** @@ -173,50 +156,45 @@ import java.util.Map; * @return A new {@link QueueTimeline} reflecting the update. */ public QueueTimeline copyWithMovedMediaItems(int fromIndex, int toIndex, int newIndex) { - List list = new ArrayList<>(mediaItems); + List list = new ArrayList<>(queuedMediaItems); Util.moveItems(list, fromIndex, toIndex, newIndex); - return new QueueTimeline( - new ImmutableList.Builder().addAll(list).build(), - mediaItemToQueueIdMap, - fakeMediaItem); + return new QueueTimeline(ImmutableList.copyOf(list), fakeMediaItem); } - /** - * Returns the media item index of the given media item in the timeline, or {@link C#INDEX_UNSET} - * if the item is not part of this timeline. - * - * @param mediaItem The media item of interest. - * @return The index of the item or {@link C#INDEX_UNSET} if the item is not part of the timeline. - */ - public int indexOf(MediaItem mediaItem) { + /** Returns whether the timeline contains the given {@link MediaItem}. */ + public boolean contains(MediaItem mediaItem) { if (mediaItem.equals(fakeMediaItem)) { - return mediaItems.size(); + return true; } - int mediaItemIndex = mediaItems.indexOf(mediaItem); - return mediaItemIndex == -1 ? C.INDEX_UNSET : mediaItemIndex; + for (int i = 0; i < queuedMediaItems.size(); i++) { + if (mediaItem.equals(queuedMediaItems.get(i).mediaItem)) { + return true; + } + } + return false; } @Nullable public MediaItem getMediaItemAt(int mediaItemIndex) { - if (mediaItemIndex >= 0 && mediaItemIndex < mediaItems.size()) { - return mediaItems.get(mediaItemIndex); + if (mediaItemIndex >= 0 && mediaItemIndex < queuedMediaItems.size()) { + return queuedMediaItems.get(mediaItemIndex).mediaItem; } - return (mediaItemIndex == mediaItems.size()) ? fakeMediaItem : null; + return (mediaItemIndex == queuedMediaItems.size()) ? fakeMediaItem : null; } @Override public int getWindowCount() { - return mediaItems.size() + ((fakeMediaItem == null) ? 0 : 1); + return queuedMediaItems.size() + ((fakeMediaItem == null) ? 0 : 1); } @Override public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { // TODO(b/149713425): Set duration if it's available from MediaMetadataCompat. MediaItem mediaItem; - if (windowIndex == mediaItems.size() && fakeMediaItem != null) { + if (windowIndex == queuedMediaItems.size() && fakeMediaItem != null) { mediaItem = fakeMediaItem; } else { - mediaItem = mediaItems.get(windowIndex); + mediaItem = queuedMediaItems.get(windowIndex).mediaItem; } return getWindow(window, mediaItem, windowIndex); } @@ -257,14 +235,13 @@ import java.util.Map; return false; } QueueTimeline other = (QueueTimeline) obj; - return Objects.equal(mediaItems, other.mediaItems) - && Objects.equal(mediaItemToQueueIdMap, other.mediaItemToQueueIdMap) + return Objects.equal(queuedMediaItems, other.queuedMediaItems) && Objects.equal(fakeMediaItem, other.fakeMediaItem); } @Override public int hashCode() { - return Objects.hashCode(mediaItems, mediaItemToQueueIdMap, fakeMediaItem); + return Objects.hashCode(queuedMediaItems, fakeMediaItem); } private static Window getWindow(Window window, MediaItem mediaItem, int windowIndex) { @@ -285,4 +262,35 @@ import java.util.Map; /* positionInFirstPeriodUs= */ 0); return window; } + + private static final class QueuedMediaItem { + + public final MediaItem mediaItem; + public final long queueId; + + public QueuedMediaItem(MediaItem mediaItem, long queueId) { + this.mediaItem = mediaItem; + this.queueId = queueId; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof QueuedMediaItem)) { + return false; + } + QueuedMediaItem that = (QueuedMediaItem) o; + return queueId == that.queueId && mediaItem.equals(that.mediaItem); + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + (int) (queueId ^ (queueId >>> 32)); + result = 31 * result + mediaItem.hashCode(); + return result; + } + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index 748fbf9fd4..8e4afdcdeb 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -78,6 +78,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.truth.os.BundleSubject; import androidx.test.filters.MediumTest; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.common.collect.Range; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -414,6 +415,41 @@ public class MediaControllerWithMediaSessionCompatTest { assertThat(timelineRef.get().getPeriodCount()).isEqualTo(0); } + @Test + public void setQueue_withDuplicatedMediaItems_updatesAndNotifiesTimeline() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference timelineFromParamRef = new AtomicReference<>(); + AtomicReference timelineFromGetterRef = new AtomicReference<>(); + AtomicInteger reasonRef = new AtomicInteger(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onTimelineChanged( + Timeline timeline, @Player.TimelineChangeReason int reason) { + timelineFromParamRef.set(timeline); + timelineFromGetterRef.set(controller.getCurrentTimeline()); + reasonRef.set(reason); + latch.countDown(); + } + }; + threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener)); + + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 2); + Timeline testTimeline = + MediaTestUtils.createTimeline( + ImmutableList.copyOf(Iterables.concat(mediaItems, mediaItems))); + List testQueue = + MediaTestUtils.convertToQueueItemsWithoutBitmap( + MediaUtils.convertToMediaItemList(testTimeline)); + session.setQueue(testQueue); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaTestUtils.assertMediaIdEquals(testTimeline, timelineFromParamRef.get()); + MediaTestUtils.assertMediaIdEquals(testTimeline, timelineFromGetterRef.get()); + assertThat(reasonRef.get()).isEqualTo(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + } + @Test public void setQueue_withDescription_notifiesTimelineWithMetadata() throws Exception { CountDownLatch latch = new CountDownLatch(1); From 5d11aa95ef24d0c50144eb4b18fca923a830b999 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 5 Apr 2023 11:30:55 +0100 Subject: [PATCH 20/35] Implement missing broadcastCustomCommand to legacy controller And also mention a few restrictions regarding legacy components in Javadoc. Issue: androidx/media#293 PiperOrigin-RevId: 522005562 (cherry picked from commit 502969a42b2ba07ada555a38841c7c65baeaa594) --- .../androidx/media3/session/MediaSession.java | 9 ++++++ .../session/MediaSessionLegacyStub.java | 5 ++++ .../media3/session/SessionCommand.java | 8 ++++- ...lerCompatCallbackWithMediaSessionTest.java | 29 +++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 3c82ec75d2..33e9af7920 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -701,6 +701,9 @@ public class MediaSession { * * * + *

    Interoperability: This call has no effect when called for a {@linkplain + * ControllerInfo#LEGACY_CONTROLLER_VERSION legacy controller}. + * * @param controller The controller to specify layout. * @param layout The ordered list of {@link CommandButton}. */ @@ -793,6 +796,9 @@ public class MediaSession { * *

    This is a synchronous call and doesn't wait for results from the controller. * + *

    Interoperability: This call has no effect when called for a {@linkplain + * ControllerInfo#LEGACY_CONTROLLER_VERSION legacy controller}. + * * @param controller The controller to send the extras to. * @param sessionExtras The session extras. */ @@ -816,6 +822,9 @@ public class MediaSession { * *

    A command is not accepted if it is not a custom command. * + *

    Interoperability: This call has no effect when called for a {@linkplain + * ControllerInfo#LEGACY_CONTROLLER_VERSION legacy controller}. + * * @param controller The controller to send the custom command to. * @param command A custom command. * @param args A {@link Bundle} for additional arguments. May be empty. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 13cf696db0..45d2db7277 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -987,6 +987,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sessionImpl.getSessionCompat().setExtras(sessionExtras); } + @Override + public void sendCustomCommand(int seq, SessionCommand command, Bundle args) { + sessionImpl.getSessionCompat().sendSessionEvent(command.customAction, args); + } + @Override public void onPlayWhenReadyChanged( int seq, boolean playWhenReady, @Player.PlaybackSuppressionReason int reason) diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java b/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java index c514af10c8..de204146e6 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionCommand.java @@ -123,6 +123,10 @@ public final class SessionCommand implements Bundleable { /** * The extra bundle of a custom command. It will be {@link Bundle#EMPTY} for a predefined command. + * + *

    Interoperability: This value is not used when the command is sent to a legacy {@link + * android.support.v4.media.session.MediaSessionCompat} or {@link + * android.support.v4.media.session.MediaControllerCompat}. */ public final Bundle customExtras; @@ -143,7 +147,9 @@ public final class SessionCommand implements Bundleable { * Creates a custom command. * * @param action The action of this custom command. - * @param extras An extra bundle for this custom command. + * @param extras An extra bundle for this custom command. This value is not used when the command + * is sent to a legacy {@link android.support.v4.media.session.MediaSessionCompat} or {@link + * android.support.v4.media.session.MediaControllerCompat}. */ public SessionCommand(String action, Bundle extras) { commandCode = COMMAND_CODE_CUSTOM; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index 71543ae0fe..bd85cf9338 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -980,6 +980,35 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { assertThat(TestUtils.equals(receivedSessionExtras.get(1), sessionExtras)).isTrue(); } + @Test + public void broadcastCustomCommand_cnSessionEventCalled() throws Exception { + Bundle commandCallExtras = new Bundle(); + commandCallExtras.putString("key-0", "value-0"); + // Specify session command extras to see that they are NOT used. + Bundle sessionCommandExtras = new Bundle(); + sessionCommandExtras.putString("key-0", "value-1"); + SessionCommand sessionCommand = new SessionCommand("custom_action", sessionCommandExtras); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference receivedCommand = new AtomicReference<>(); + AtomicReference receivedCommandExtras = new AtomicReference<>(); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onSessionEvent(String event, Bundle extras) { + receivedCommand.set(event); + receivedCommandExtras.set(extras); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.broadcastCustomCommand(sessionCommand, commandCallExtras); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedCommand.get()).isEqualTo("custom_action"); + assertThat(TestUtils.equals(receivedCommandExtras.get(), commandCallExtras)).isTrue(); + } + @Test public void onMediaItemTransition_updatesLegacyMetadataAndPlaybackState_correctModelConversion() throws Exception { From 8aec88b2b4dfac621f7121182c4036a204314177 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 6 Apr 2023 09:30:53 +0100 Subject: [PATCH 21/35] Add missing initial update to the subtitle button in the session demo The button gets only updated after an onTracksChanged callback but isn't set to the initial state similar to other changes triggered by callbacks (e.g. updateRepeatSwitchUI) PiperOrigin-RevId: 522274800 (cherry picked from commit 14ba173dfea5f077d2371b5f5d01313551ecdf60) --- .../src/main/java/androidx/media3/demo/session/PlayerActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt index 9930594857..e4f09b8d33 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt @@ -135,6 +135,7 @@ class PlayerActivity : AppCompatActivity() { updateMediaMetadataUI(controller.mediaMetadata) updateShuffleSwitchUI(controller.shuffleModeEnabled) updateRepeatSwitchUI(controller.repeatMode) + playerView.setShowSubtitleButton(controller.currentTracks.isTypeSupported(TRACK_TYPE_TEXT)) controller.addListener( object : Player.Listener { From 0690c9ba6762fec3bf42437bb580eb845b86d1d1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 11 Apr 2023 12:14:00 +0100 Subject: [PATCH 22/35] Replace deprecated onBackPressed call PiperOrigin-RevId: 523361561 (cherry picked from commit be85684dc963faf2695f1111cf6f82f257fa6e9e) --- .../androidx/media3/demo/session/MainActivity.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt index e012125cb9..9328a059e9 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/MainActivity.kt @@ -26,6 +26,7 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.ListView import android.widget.TextView +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.media3.common.MediaItem @@ -73,21 +74,24 @@ class MainActivity : AppCompatActivity() { val intent = Intent(this, PlayerActivity::class.java) startActivity(intent) } + + onBackPressedDispatcher.addCallback( + object : OnBackPressedCallback(/* enabled= */ true) { + override fun handleOnBackPressed() { + popPathStack() + } + } + ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) } - @SuppressWarnings("MissingSuperCall") - override fun onBackPressed() { - popPathStack() - } - override fun onStart() { super.onStart() initializeBrowser() From abc9d8ceda6c55a665c150f6ed002650d7c4aee9 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 11 Apr 2023 19:25:33 +0100 Subject: [PATCH 23/35] Change format logged when AudioSink throws InitializationException Change what format is logged from MediaCodecAudioRenderer when AudioSink throws InitializationException. We printed the AudioSink's format, which most of the times is audio/raw (PCM) and not the renderer's format. With this change both formats are logged. #minor-release Issue: google/ExoPlayer#11066 PiperOrigin-RevId: 523456840 (cherry picked from commit baf1aa1cdbe626097c7fa310047edac7033f2ffe) --- .../java/androidx/media3/exoplayer/audio/AudioSink.java | 2 ++ .../media3/exoplayer/audio/MediaCodecAudioRenderer.java | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java index 20f95192db..b9fc644357 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java @@ -189,6 +189,8 @@ public interface AudioSink { + audioTrackState + " " + ("Config(" + sampleRate + ", " + channelConfig + ", " + bufferSize + ")") + + " " + + format + (isRecoverable ? " (recoverable)" : ""), audioTrackException); this.audioTrackState = audioTrackState; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java index 2f63b80cf8..5f02f40c12 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java @@ -105,6 +105,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private int codecMaxInputSize; private boolean codecNeedsDiscardChannelsWorkaround; + @Nullable private Format inputFormat; /** Codec used for DRM decryption only in passthrough and offload. */ @Nullable private Format decryptOnlyCodecFormat; @@ -500,8 +501,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + inputFormat = checkNotNull(formatHolder.format); @Nullable DecoderReuseEvaluation evaluation = super.onInputFormatChanged(formatHolder); - eventDispatcher.inputFormatChanged(formatHolder.format, evaluation); + eventDispatcher.inputFormatChanged(inputFormat, evaluation); return evaluation; } @@ -604,6 +606,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onDisabled() { audioSinkNeedsReset = true; + inputFormat = null; try { audioSink.flush(); } finally { @@ -711,7 +714,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media fullyConsumed = audioSink.handleBuffer(buffer, bufferPresentationTimeUs, sampleCount); } catch (InitializationException e) { throw createRendererException( - e, e.format, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED); + e, inputFormat, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED); } catch (WriteException e) { throw createRendererException( e, format, e.isRecoverable, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED); From 93605303adf8b12df5ba4f238c08dbd8dbe700a2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 12 Apr 2023 09:33:46 +0100 Subject: [PATCH 24/35] Update available commands when setting a new player in MediaSession PiperOrigin-RevId: 523633865 (cherry picked from commit ae875648a72b08f24c1e41cc5822fc24265dfd9b) --- .../media3/session/MediaSessionImpl.java | 29 ++++++++++--------- .../test/session/common/CommonConstants.java | 1 + .../session/MediaControllerListenerTest.java | 13 ++++++++- .../session/MediaSessionProviderService.java | 5 ++++ .../media3/session/RemoteMediaSession.java | 7 +++++ 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 67acd1aca6..acaa1ecf3b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -15,7 +15,6 @@ */ package androidx.media3.session; -import static androidx.media3.common.Player.COMMAND_GET_TRACKS; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.castNonNull; @@ -278,8 +277,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; } playerInfo = newPlayerWrapper.createPlayerInfoForBundling(); - onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( - /* excludeTimeline= */ false, /* excludeTracks= */ false); + handleAvailablePlayerCommandsChanged(newPlayerWrapper.getAvailableCommands()); } public void release() { @@ -772,6 +770,20 @@ import org.checkerframework.checker.initialization.qual.Initialized; } } + private void handleAvailablePlayerCommandsChanged(Player.Commands availableCommands) { + // Update PlayerInfo and do not force exclude elements in case they need to be updated because + // an available command has been removed. + onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ false); + dispatchRemoteControllerTaskWithoutReturn( + (callback, seq) -> callback.onAvailableCommandsChangedFromPlayer(seq, availableCommands)); + + // Forcefully update playback info to update VolumeProviderCompat in case + // COMMAND_ADJUST_DEVICE_VOLUME or COMMAND_SET_DEVICE_VOLUME value has changed. + dispatchRemoteControllerTaskToLegacyStub( + (callback, seq) -> callback.onDeviceInfoChanged(seq, playerInfo.deviceInfo)); + } + /* @FunctionalInterface */ interface RemoteControllerTask { @@ -1182,16 +1194,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; if (player == null) { return; } - boolean excludeTracks = !availableCommands.contains(COMMAND_GET_TRACKS); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( - /* excludeTimeline= */ false, excludeTracks); - session.dispatchRemoteControllerTaskWithoutReturn( - (callback, seq) -> callback.onAvailableCommandsChangedFromPlayer(seq, availableCommands)); - - // Forcefully update playback info to update VolumeProviderCompat in case - // COMMAND_ADJUST_DEVICE_VOLUME or COMMAND_SET_DEVICE_VOLUME value has changed. - session.dispatchRemoteControllerTaskToLegacyStub( - (callback, seq) -> callback.onDeviceInfoChanged(seq, session.playerInfo.deviceInfo)); + session.handleAvailablePlayerCommandsChanged(availableCommands); } @Override diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java index b7c70c4825..a49212028f 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/CommonConstants.java @@ -103,6 +103,7 @@ public class CommonConstants { public static final String KEY_MAX_SEEK_TO_PREVIOUS_POSITION_MS = "maxSeekToPreviousPositionMs"; public static final String KEY_TRACK_SELECTION_PARAMETERS = "trackSelectionParameters"; public static final String KEY_CURRENT_TRACKS = "currentTracks"; + public static final String KEY_AVAILABLE_COMMANDS = "availableCommands"; // SessionCompat arguments public static final String KEY_SESSION_COMPAT_TOKEN = "sessionCompatToken"; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index 0beb95bef4..0a195d6ae1 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -326,6 +326,8 @@ public class MediaControllerListenerTest { @Player.RepeatMode int testRepeatMode = Player.REPEAT_MODE_ALL; int testCurrentAdGroupIndex = 33; int testCurrentAdIndexInAdGroup = 11; + Commands testCommands = + new Commands.Builder().addAllCommands().remove(Player.COMMAND_STOP).build(); AtomicInteger stateRef = new AtomicInteger(); AtomicReference timelineRef = new AtomicReference<>(); AtomicReference playlistMetadataRef = new AtomicReference<>(); @@ -335,7 +337,8 @@ public class MediaControllerListenerTest { AtomicInteger currentAdIndexInAdGroupRef = new AtomicInteger(); AtomicBoolean shuffleModeEnabledRef = new AtomicBoolean(); AtomicInteger repeatModeRef = new AtomicInteger(); - CountDownLatch latch = new CountDownLatch(7); + AtomicReference commandsRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(8); MediaController controller = controllerTestRule.createController(remoteSession.getToken()); threadTestRule .getHandler() @@ -343,6 +346,12 @@ public class MediaControllerListenerTest { () -> controller.addListener( new Player.Listener() { + @Override + public void onAvailableCommandsChanged(Commands availableCommands) { + commandsRef.set(availableCommands); + latch.countDown(); + } + @Override public void onAudioAttributesChanged(AudioAttributes attributes) { audioAttributesRef.set(attributes); @@ -402,6 +411,7 @@ public class MediaControllerListenerTest { .setIsPlayingAd(true) .setCurrentAdGroupIndex(testCurrentAdGroupIndex) .setCurrentAdIndexInAdGroup(testCurrentAdIndexInAdGroup) + .setAvailableCommands(testCommands) .build(); remoteSession.setPlayer(playerConfig); @@ -415,6 +425,7 @@ public class MediaControllerListenerTest { assertThat(currentAdIndexInAdGroupRef.get()).isEqualTo(testCurrentAdIndexInAdGroup); assertThat(shuffleModeEnabledRef.get()).isEqualTo(testShuffleModeEnabled); assertThat(repeatModeRef.get()).isEqualTo(testRepeatMode); + assertThat(commandsRef.get()).isEqualTo(testCommands); } @Test diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java index d48253eed7..f70c6396d6 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java @@ -18,6 +18,7 @@ package androidx.media3.session; import static androidx.media3.common.Player.COMMAND_GET_TRACKS; import static androidx.media3.test.session.common.CommonConstants.ACTION_MEDIA3_SESSION; import static androidx.media3.test.session.common.CommonConstants.KEY_AUDIO_ATTRIBUTES; +import static androidx.media3.test.session.common.CommonConstants.KEY_AVAILABLE_COMMANDS; import static androidx.media3.test.session.common.CommonConstants.KEY_BUFFERED_PERCENTAGE; import static androidx.media3.test.session.common.CommonConstants.KEY_BUFFERED_POSITION; import static androidx.media3.test.session.common.CommonConstants.KEY_CONTENT_BUFFERED_POSITION; @@ -397,6 +398,10 @@ public class MediaSessionProviderService extends Service { player.trackSelectionParameters = TrackSelectionParameters.fromBundle(trackSelectionParametersBundle); } + @Nullable Bundle availableCommandsBundle = config.getBundle(KEY_AVAILABLE_COMMANDS); + if (availableCommandsBundle != null) { + player.commands = Player.Commands.CREATOR.fromBundle(availableCommandsBundle); + } return player; } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java index ca2840480d..dff8fc65c8 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java @@ -17,6 +17,7 @@ package androidx.media3.session; import static androidx.media3.test.session.common.CommonConstants.ACTION_MEDIA3_SESSION; import static androidx.media3.test.session.common.CommonConstants.KEY_AUDIO_ATTRIBUTES; +import static androidx.media3.test.session.common.CommonConstants.KEY_AVAILABLE_COMMANDS; import static androidx.media3.test.session.common.CommonConstants.KEY_BUFFERED_PERCENTAGE; import static androidx.media3.test.session.common.CommonConstants.KEY_BUFFERED_POSITION; import static androidx.media3.test.session.common.CommonConstants.KEY_CONTENT_BUFFERED_POSITION; @@ -742,6 +743,12 @@ public class RemoteMediaSession { return this; } + @CanIgnoreReturnValue + public MockPlayerConfigBuilder setAvailableCommands(Player.Commands availableCommands) { + bundle.putBundle(KEY_AVAILABLE_COMMANDS, availableCommands.toBundle()); + return this; + } + public Bundle build() { return bundle; } From eb322b7c2e5bacde17c99056f22e4e6fba27dc49 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 12 Apr 2023 10:00:03 +0100 Subject: [PATCH 25/35] Precedence for app provided media button receiver This change selects the best suited media button receiver component and pending intent when creating the legacy session. This is important to ensure that a service can be started with a media button event from BT headsets after the app has been terminated. The `MediaSessionLegacyStub` selects the best suited receiver to be passed to the `MediaSessionCompat` constructor. 1. When the app has declared a broadcast receiver for `ACTION_MEDIA_BUTTON` in the manifest, this broadcast receiver is used. 2. When the session is housed in a service, the service component is used as a fallback. 3. As a last resort a receiver is created at runtime. When the `MediaSessionLegacyStub` is released, the media button receiver is removed unless the app has provided a media button receiver in the manifest. In this case we assume the app supports resuming when the BT play intent arrives at `MediaSessionService.onStartCommand`. Issue: androidx/media#167 Issue: androidx/media#27 Issue: androidx/media#314 PiperOrigin-RevId: 523638051 (cherry picked from commit e54a9343980c7f6c44ddf0edbb938dd5f6b11193) --- .../media3/session/MediaSessionImpl.java | 72 +----------- .../session/MediaSessionLegacyStub.java | 105 +++++++++++++++++- 2 files changed, 107 insertions(+), 70 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index acaa1ecf3b..13738587b4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -17,18 +17,15 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; -import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED; import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import android.app.PendingIntent; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; @@ -42,7 +39,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.support.v4.media.session.MediaSessionCompat; -import android.view.KeyEvent; import androidx.annotation.FloatRange; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; @@ -65,7 +61,6 @@ import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Log; -import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; @@ -114,8 +109,6 @@ import org.checkerframework.checker.initialization.qual.Initialized; private final SessionToken sessionToken; private final MediaSession instance; @Nullable private final PendingIntent sessionActivity; - private final PendingIntent mediaButtonIntent; - @Nullable private final BroadcastReceiver broadcastReceiver; private final Handler applicationHandler; private final BitmapLoader bitmapLoader; private final Runnable periodicSessionPositionInfoUpdateRunnable; @@ -188,52 +181,21 @@ import org.checkerframework.checker.initialization.qual.Initialized; sessionStub, tokenExtras); - @Nullable ComponentName mbrComponent; synchronized (STATIC_LOCK) { if (!componentNamesInitialized) { - serviceComponentName = + MediaSessionImpl.serviceComponentName = getServiceComponentByAction(context, MediaLibraryService.SERVICE_INTERFACE); - if (serviceComponentName == null) { - serviceComponentName = + if (MediaSessionImpl.serviceComponentName == null) { + MediaSessionImpl.serviceComponentName = getServiceComponentByAction(context, MediaSessionService.SERVICE_INTERFACE); } componentNamesInitialized = true; } - mbrComponent = serviceComponentName; - } - int pendingIntentFlagMutable = Util.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0; - if (mbrComponent == null) { - // No service to revive playback after it's dead. - // Create a PendingIntent that points to the runtime broadcast receiver. - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionUri); - intent.setPackage(context.getPackageName()); - mediaButtonIntent = - PendingIntent.getBroadcast( - context, /* requestCode= */ 0, intent, pendingIntentFlagMutable); - - // Creates a fake ComponentName for MediaSessionCompat in pre-L. - mbrComponent = new ComponentName(context, context.getClass()); - - // Create and register a BroadcastReceiver for receiving PendingIntent. - broadcastReceiver = new MediaButtonReceiver(); - IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON); - filter.addDataScheme(castNonNull(sessionUri.getScheme())); - Util.registerReceiverNotExported(context, broadcastReceiver, filter); - } else { - // Has MediaSessionService to revive playback after it's dead. - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionUri); - intent.setComponent(mbrComponent); - if (Util.SDK_INT >= 26) { - mediaButtonIntent = - PendingIntent.getForegroundService(context, 0, intent, pendingIntentFlagMutable); - } else { - mediaButtonIntent = PendingIntent.getService(context, 0, intent, pendingIntentFlagMutable); - } - broadcastReceiver = null; } sessionLegacyStub = - new MediaSessionLegacyStub(thisRef, mbrComponent, mediaButtonIntent, applicationHandler); + new MediaSessionLegacyStub( + /* session= */ thisRef, sessionUri, serviceComponentName, applicationHandler); PlayerWrapper playerWrapper = new PlayerWrapper(player); this.playerWrapper = playerWrapper; @@ -303,10 +265,6 @@ import org.checkerframework.checker.initialization.qual.Initialized; Log.w(TAG, "Exception thrown while closing", e); } sessionLegacyStub.release(); - mediaButtonIntent.cancel(); - if (broadcastReceiver != null) { - context.unregisterReceiver(broadcastReceiver); - } sessionStub.release(); } @@ -1284,26 +1242,6 @@ import org.checkerframework.checker.initialization.qual.Initialized; } } - // TODO(b/193193462): Replace this with androidx.media.session.MediaButtonReceiver - private final class MediaButtonReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - if (!Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { - return; - } - Uri sessionUri = intent.getData(); - if (!Util.areEqual(sessionUri, MediaSessionImpl.this.sessionUri)) { - return; - } - KeyEvent keyEvent = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (keyEvent == null) { - return; - } - getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); - } - } - private class PlayerInfoChangedHandler extends Handler { private static final int MSG_PLAYER_INFO_CHANGED = 1; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 45d2db7277..fe8f91832e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -35,6 +35,7 @@ import static androidx.media3.common.Player.STATE_ENDED; import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM; @@ -43,9 +44,13 @@ import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; @@ -107,6 +112,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static final String TAG = "MediaSessionLegacyStub"; + private static final int PENDING_INTENT_FLAG_MUTABLE = + Util.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0; private static final String DEFAULT_MEDIA_SESSION_TAG_PREFIX = "androidx.media3.session.id"; private static final String DEFAULT_MEDIA_SESSION_TAG_DELIM = "."; @@ -122,6 +129,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final MediaPlayPauseKeyHandler mediaPlayPauseKeyHandler; private final MediaSessionCompat sessionCompat; private final String appPackageName; + @Nullable private final MediaButtonReceiver runtimeBroadcastReceiver; + private final boolean canResumePlaybackOnStart; @Nullable private VolumeProviderCompat volumeProviderCompat; private volatile long connectionTimeoutMs; @@ -130,8 +139,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public MediaSessionLegacyStub( MediaSessionImpl session, - ComponentName mbrComponent, - PendingIntent mediaButtonIntent, + Uri sessionUri, + @Nullable ComponentName serviceComponentName, Handler handler) { sessionImpl = session; Context context = sessionImpl.getContext(); @@ -145,6 +154,44 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; connectedControllersManager = new ConnectedControllersManager<>(session); connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS; + // Select a media button receiver component. + ComponentName receiverComponentName = queryPackageManagerForMediaButtonReceiver(context); + // Assume an app that intentionally puts a `MediaButtonReceiver` into the manifest has + // implemented some kind of resumption of the last recently played media item. + canResumePlaybackOnStart = receiverComponentName != null; + if (receiverComponentName == null) { + receiverComponentName = serviceComponentName; + } + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, sessionUri); + PendingIntent mediaButtonIntent; + if (receiverComponentName == null) { + // Neither a media button receiver from the app manifest nor a service available that could + // handle media button events. Create a runtime receiver and a pending intent for it. + runtimeBroadcastReceiver = new MediaButtonReceiver(); + IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON); + filter.addDataScheme(castNonNull(sessionUri.getScheme())); + Util.registerReceiverNotExported(context, runtimeBroadcastReceiver, filter); + // Create a pending intent to be broadcast to the receiver. + intent.setPackage(context.getPackageName()); + mediaButtonIntent = + PendingIntent.getBroadcast( + context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE); + // Creates a fake ComponentName for MediaSessionCompat in pre-L. + receiverComponentName = new ComponentName(context, context.getClass()); + } else { + intent.setComponent(receiverComponentName); + mediaButtonIntent = + Objects.equals(serviceComponentName, receiverComponentName) + ? (Util.SDK_INT >= 26 + ? PendingIntent.getForegroundService( + context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE) + : PendingIntent.getService( + context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE)) + : PendingIntent.getBroadcast( + context, /* requestCode= */ 0, intent, PENDING_INTENT_FLAG_MUTABLE); + runtimeBroadcastReceiver = null; + } + String sessionCompatId = TextUtils.join( DEFAULT_MEDIA_SESSION_TAG_DELIM, @@ -153,7 +200,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; new MediaSessionCompat( context, sessionCompatId, - mbrComponent, + receiverComponentName, mediaButtonIntent, session.getToken().getExtras()); @@ -168,12 +215,38 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sessionCompat.setCallback(thisRef, handler); } + @Nullable + private static ComponentName queryPackageManagerForMediaButtonReceiver(Context context) { + PackageManager pm = context.getPackageManager(); + Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + queryIntent.setPackage(context.getPackageName()); + List resolveInfos = pm.queryBroadcastReceivers(queryIntent, /* flags= */ 0); + if (resolveInfos.size() == 1) { + ResolveInfo resolveInfo = resolveInfos.get(0); + return new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); + } else if (resolveInfos.isEmpty()) { + return null; + } else { + throw new IllegalStateException( + "Expected 1 broadcast receiver that handles " + + Intent.ACTION_MEDIA_BUTTON + + ", found " + + resolveInfos.size()); + } + } + /** Starts to receive commands. */ public void start() { sessionCompat.setActive(true); } public void release() { + if (!canResumePlaybackOnStart) { + setMediaButtonReceiver(sessionCompat, /* mediaButtonReceiverIntent= */ null); + } + if (runtimeBroadcastReceiver != null) { + sessionImpl.getContext().unregisterReceiver(runtimeBroadcastReceiver); + } sessionCompat.release(); } @@ -832,6 +905,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; sessionCompat.setMetadata(metadataCompat); } + @SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable. + private static void setMediaButtonReceiver( + MediaSessionCompat sessionCompat, @Nullable PendingIntent mediaButtonReceiverIntent) { + sessionCompat.setMediaButtonReceiver(mediaButtonReceiverIntent); + } + @SuppressWarnings("nullness:argument") // MediaSessionCompat didn't annotate @Nullable. private static void setQueue(MediaSessionCompat sessionCompat, @Nullable List queue) { sessionCompat.setQueue(queue); @@ -1358,4 +1437,24 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static String getBitmapLoadErrorMessage(Throwable throwable) { return "Failed to load bitmap: " + throwable.getMessage(); } + + // TODO(b/193193462): Replace this with androidx.media.session.MediaButtonReceiver + private final class MediaButtonReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (!Util.areEqual(intent.getAction(), Intent.ACTION_MEDIA_BUTTON)) { + return; + } + Uri sessionUri = intent.getData(); + if (!Util.areEqual(sessionUri, sessionUri)) { + return; + } + KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (keyEvent == null) { + return; + } + getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); + } + } } From 1076756d1cbff6d296fe4c948dd159d97b56498f Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 12 Apr 2023 16:35:02 +0100 Subject: [PATCH 26/35] Enable multidex for demo-gl app #minor-release PiperOrigin-RevId: 523708424 (cherry picked from commit 81fd9d2867602551602d3bde9a95e8c268593d18) --- demos/gl/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle index ff8333a254..8666c45aa0 100644 --- a/demos/gl/build.gradle +++ b/demos/gl/build.gradle @@ -27,6 +27,7 @@ android { versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.appTargetSdkVersion + multiDexEnabled true } buildTypes { From ade88311666386fac6914241ea6bd9755a065e0b Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 13 Apr 2023 11:58:20 +0100 Subject: [PATCH 27/35] Bump version numbers to Media3 1.0.1 and ExoPlayer 2.18.6 #minor-release PiperOrigin-RevId: 523959161 (cherry picked from commit e033dbac03bfa0eb2aa13f855360fafe9f1bd488) --- .github/ISSUE_TEMPLATE/bug.yml | 2 ++ constants.gradle | 4 ++-- .../main/java/androidx/media3/common/MediaLibraryInfo.java | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 3851f7b13c..e6dde5ad63 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -20,6 +20,7 @@ body: label: Media3 Version description: What version of Media3 (or ExoPlayer) are you using? options: + - Media3 1.0.1 - Media3 1.0.0 - Media3 1.0.0-rc02 - Media3 1.0.0-rc01 @@ -29,6 +30,7 @@ body: - Media3 1.0.0-alpha03 - Media3 1.0.0-alpha02 - Media3 1.0.0-alpha01 + - ExoPlayer 2.18.6 - ExoPlayer 2.18.5 - ExoPlayer 2.18.4 - ExoPlayer 2.18.3 diff --git a/constants.gradle b/constants.gradle index c07fb57692..dac2a21c37 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { - releaseVersion = '1.0.0' - releaseVersionCode = 1_000_000_3_00 + releaseVersion = '1.0.1' + releaseVersionCode = 1_000_001_3_00 minSdkVersion = 16 appTargetSdkVersion = 33 // API version before restricting local file access. diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java index f1f2f0a81c..3620406bfb 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java @@ -29,11 +29,11 @@ public final class MediaLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "1.0.0"; + public static final String VERSION = "1.0.1"; /** The version of the library expressed as {@code TAG + "/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.1"; /** * The version of the library expressed as an integer, for example 1002003300. @@ -47,7 +47,7 @@ public final class MediaLibraryInfo { * (123-045-006-3-00). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 1_000_000_3_00; + public static final int VERSION_INT = 1_000_001_3_00; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true; From ba6ccee802764c859368571c16a87e23ccea8eee Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 13 Apr 2023 14:25:27 +0100 Subject: [PATCH 28/35] Update `colr` box values to be overridden by bitstream boxes #minor-release PiperOrigin-RevId: 523983688 (cherry picked from commit 596a7c7033ba061b48ac4edd5647c6d2fb813a2d) --- .../media3/extractor/mp4/AtomParsers.java | 71 +++++++++---------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index 3d2a61dcec..a405753f24 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1164,15 +1164,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; pixelWidthHeightRatio = hevcConfig.pixelWidthHeightRatio; } codecs = hevcConfig.codecs; - // Modify these values only if they have not already been set. If 'Atom.TYPE_colr' atom is - // present, these values may be overridden. - if (colorSpace == Format.NO_VALUE - && colorRange == Format.NO_VALUE - && colorTransfer == Format.NO_VALUE) { - colorSpace = hevcConfig.colorSpace; - colorRange = hevcConfig.colorRange; - colorTransfer = hevcConfig.colorTransfer; - } + colorSpace = hevcConfig.colorSpace; + colorRange = hevcConfig.colorRange; + colorTransfer = hevcConfig.colorTransfer; } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { @Nullable DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); if (dolbyVisionConfig != null) { @@ -1188,16 +1182,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; boolean fullRangeFlag = (parent.readUnsignedByte() & 1) != 0; int colorPrimaries = parent.readUnsignedByte(); int transferCharacteristics = parent.readUnsignedByte(); - // Modify these values only if they have not already been set. If 'Atom.TYPE_colr' atom is - // present, these values may be overridden. - if (colorSpace == Format.NO_VALUE - && colorRange == Format.NO_VALUE - && colorTransfer == Format.NO_VALUE) { - colorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries); - colorRange = fullRangeFlag ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; - colorTransfer = - ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics); - } + colorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries); + colorRange = fullRangeFlag ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; + colorTransfer = + ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics); } else if (childAtomType == Atom.TYPE_av1C) { ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null); mimeType = MimeTypes.VIDEO_AV1; @@ -1277,26 +1265,33 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } } else if (childAtomType == Atom.TYPE_colr) { - int colorType = parent.readInt(); - if (colorType == TYPE_nclx || colorType == TYPE_nclc) { - // For more info on syntax, see Section 8.5.2.2 in ISO/IEC 14496-12:2012(E) and - // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html. - int colorPrimaries = parent.readUnsignedShort(); - int transferCharacteristics = parent.readUnsignedShort(); - parent.skipBytes(2); // matrix_coefficients. + // Only modify these values if they have not been previously established by the bitstream. + // If 'Atom.TYPE_hvcC' atom or 'Atom.TYPE_vpcC' is available, they will take precedence and + // overwrite any existing values. + if (colorSpace == Format.NO_VALUE + && colorRange == Format.NO_VALUE + && colorTransfer == Format.NO_VALUE) { + int colorType = parent.readInt(); + if (colorType == TYPE_nclx || colorType == TYPE_nclc) { + // For more info on syntax, see Section 8.5.2.2 in ISO/IEC 14496-12:2012(E) and + // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html. + int colorPrimaries = parent.readUnsignedShort(); + int transferCharacteristics = parent.readUnsignedShort(); + parent.skipBytes(2); // matrix_coefficients. - // Only try and read full_range_flag if the box is long enough. It should be present in - // all colr boxes with type=nclx (Section 8.5.2.2 in ISO/IEC 14496-12:2012(E)) but some - // device cameras record videos with type=nclx without this final flag (and therefore - // size=18): https://github.com/google/ExoPlayer/issues/9332 - boolean fullRangeFlag = - childAtomSize == 19 && (parent.readUnsignedByte() & 0b10000000) != 0; - colorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries); - colorRange = fullRangeFlag ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; - colorTransfer = - ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics); - } else { - Log.w(TAG, "Unsupported color type: " + Atom.getAtomTypeString(colorType)); + // Only try and read full_range_flag if the box is long enough. It should be present in + // all colr boxes with type=nclx (Section 8.5.2.2 in ISO/IEC 14496-12:2012(E)) but some + // device cameras record videos with type=nclx without this final flag (and therefore + // size=18): https://github.com/google/ExoPlayer/issues/9332 + boolean fullRangeFlag = + childAtomSize == 19 && (parent.readUnsignedByte() & 0b10000000) != 0; + colorSpace = ColorInfo.isoColorPrimariesToColorSpace(colorPrimaries); + colorRange = fullRangeFlag ? C.COLOR_RANGE_FULL : C.COLOR_RANGE_LIMITED; + colorTransfer = + ColorInfo.isoTransferCharacteristicsToColorTransfer(transferCharacteristics); + } else { + Log.w(TAG, "Unsupported color type: " + Atom.getAtomTypeString(colorType)); + } } } childPosition += childAtomSize; From 3b2b8d528dd49a38386bfb9b7ce4dfd2e26589b2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 13 Apr 2023 14:42:24 +0100 Subject: [PATCH 29/35] Ensure TrackSelectionParameters overrides match existing groups The overrides specified by a MediaController may not use the exact same TrackGroup instances as known to the Player because the groups have been bundled to and from the controller. This bundling may alter the instance slightly depending on the version used on each side of the communication and the fields set (e.g. Format.metadata is not supported for bundling). This issue can be solved by creating unique track group ids for each group on the session side before bundling. On the way back, the groups in the track selection parameters can be mapped backed to their original instances based on this id. Issue: androidx/media#296 PiperOrigin-RevId: 523986626 (cherry picked from commit 1c557e2fd18c77243474af2f8c99fa6ea061b38a) --- .../java/androidx/media3/common/Tracks.java | 12 ++ .../media3/session/MediaSessionImpl.java | 2 +- .../media3/session/MediaSessionStub.java | 74 ++++++++++- .../session/MediaControllerListenerTest.java | 9 +- .../media3/session/MediaControllerTest.java | 116 +++++++++++++++++- .../MediaSessionAndControllerTest.java | 62 ++++++++++ 6 files changed, 268 insertions(+), 7 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Tracks.java b/libraries/common/src/main/java/androidx/media3/common/Tracks.java index 734a8306b8..a932ac6924 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Tracks.java +++ b/libraries/common/src/main/java/androidx/media3/common/Tracks.java @@ -191,6 +191,18 @@ public final class Tracks implements Bundleable { return mediaTrackGroup.type; } + /** + * Copies the {@code Group} with a new {@link TrackGroup#id}. + * + * @param groupId The new {@link TrackGroup#id} + * @return The copied {@code Group}. + */ + @UnstableApi + public Group copyWithId(String groupId) { + return new Group( + mediaTrackGroup.copyWithId(groupId), adaptiveSupported, trackSupport, trackSelected); + } + @Override public boolean equals(@Nullable Object other) { if (this == other) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 13738587b4..95082214a4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -351,7 +351,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; private void dispatchOnPlayerInfoChanged( PlayerInfo playerInfo, boolean excludeTimeline, boolean excludeTracks) { - + playerInfo = sessionStub.generateAndCacheUniqueTrackGroupIds(playerInfo); List controllers = sessionStub.getConnectedControllersManager().getConnectedControllers(); for (int i = 0; i < controllers.size(); i++) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 3c0b27a775..2480dd9951 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -70,7 +70,10 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Rating; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.Tracks; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Consumer; @@ -82,6 +85,7 @@ import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SessionCommand.CommandCode; +import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -113,6 +117,9 @@ import java.util.concurrent.ExecutionException; private final ConnectedControllersManager connectedControllersManager; private final Set pendingControllers; + private ImmutableBiMap trackGroupIdMap; + private int nextUniqueTrackGroupIdPrefix; + public MediaSessionStub(MediaSessionImpl sessionImpl) { // Initialize members with params. this.sessionImpl = new WeakReference<>(sessionImpl); @@ -120,6 +127,7 @@ import java.util.concurrent.ExecutionException; connectedControllersManager = new ConnectedControllersManager<>(sessionImpl); // ConcurrentHashMap has a bug in APIs 21-22 that can result in lost updates. pendingControllers = Collections.synchronizedSet(new HashSet<>()); + trackGroupIdMap = ImmutableBiMap.of(); } public ConnectedControllersManager getConnectedControllersManager() { @@ -493,6 +501,7 @@ import java.util.concurrent.ExecutionException; // session/controller. PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); PlayerInfo playerInfo = playerWrapper.createPlayerInfoForBundling(); + playerInfo = generateAndCacheUniqueTrackGroupIds(playerInfo); ConnectionState state = new ConnectionState( MediaLibraryInfo.VERSION_INT, @@ -1435,7 +1444,11 @@ import java.util.concurrent.ExecutionException; sequenceNumber, COMMAND_SET_TRACK_SELECTION_PARAMETERS, sendSessionResultSuccess( - player -> player.setTrackSelectionParameters(trackSelectionParameters))); + player -> { + TrackSelectionParameters updatedParameters = + updateOverridesUsingUniqueTrackGroupIds(trackSelectionParameters); + player.setTrackSelectionParameters(updatedParameters); + })); } ////////////////////////////////////////////////////////////////////////////////////////////// @@ -1622,6 +1635,65 @@ import java.util.concurrent.ExecutionException; librarySessionImpl.onUnsubscribeOnHandler(controller, parentId))); } + /* package */ PlayerInfo generateAndCacheUniqueTrackGroupIds(PlayerInfo playerInfo) { + ImmutableList trackGroups = playerInfo.currentTracks.getGroups(); + ImmutableList.Builder updatedTrackGroups = ImmutableList.builder(); + ImmutableBiMap.Builder updatedTrackGroupIdMap = ImmutableBiMap.builder(); + for (int i = 0; i < trackGroups.size(); i++) { + Tracks.Group trackGroup = trackGroups.get(i); + TrackGroup mediaTrackGroup = trackGroup.getMediaTrackGroup(); + @Nullable String uniqueId = trackGroupIdMap.get(mediaTrackGroup); + if (uniqueId == null) { + uniqueId = generateUniqueTrackGroupId(mediaTrackGroup); + } + updatedTrackGroupIdMap.put(mediaTrackGroup, uniqueId); + updatedTrackGroups.add(trackGroup.copyWithId(uniqueId)); + } + trackGroupIdMap = updatedTrackGroupIdMap.buildOrThrow(); + playerInfo = playerInfo.copyWithCurrentTracks(new Tracks(updatedTrackGroups.build())); + if (playerInfo.trackSelectionParameters.overrides.isEmpty()) { + return playerInfo; + } + TrackSelectionParameters.Builder updatedTrackSelectionParameters = + playerInfo.trackSelectionParameters.buildUpon().clearOverrides(); + for (TrackSelectionOverride override : playerInfo.trackSelectionParameters.overrides.values()) { + TrackGroup trackGroup = override.mediaTrackGroup; + @Nullable String uniqueId = trackGroupIdMap.get(trackGroup); + if (uniqueId != null) { + updatedTrackSelectionParameters.addOverride( + new TrackSelectionOverride(trackGroup.copyWithId(uniqueId), override.trackIndices)); + } else { + updatedTrackSelectionParameters.addOverride(override); + } + } + return playerInfo.copyWithTrackSelectionParameters(updatedTrackSelectionParameters.build()); + } + + private TrackSelectionParameters updateOverridesUsingUniqueTrackGroupIds( + TrackSelectionParameters trackSelectionParameters) { + if (trackSelectionParameters.overrides.isEmpty()) { + return trackSelectionParameters; + } + TrackSelectionParameters.Builder updateTrackSelectionParameters = + trackSelectionParameters.buildUpon().clearOverrides(); + for (TrackSelectionOverride override : trackSelectionParameters.overrides.values()) { + TrackGroup trackGroup = override.mediaTrackGroup; + @Nullable TrackGroup originalTrackGroup = trackGroupIdMap.inverse().get(trackGroup.id); + if (originalTrackGroup != null + && override.mediaTrackGroup.length == originalTrackGroup.length) { + updateTrackSelectionParameters.addOverride( + new TrackSelectionOverride(originalTrackGroup, override.trackIndices)); + } else { + updateTrackSelectionParameters.addOverride(override); + } + } + return updateTrackSelectionParameters.build(); + } + + private String generateUniqueTrackGroupId(TrackGroup trackGroup) { + return Util.intToStringMaxRadix(nextUniqueTrackGroupIdPrefix++) + "-" + trackGroup.id; + } + /** Common interface for code snippets to handle all incoming commands from the controller. */ private interface SessionTask { T run(K sessionImpl, ControllerInfo controller, int sequenceNumber); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index 0a195d6ae1..a81ef25d51 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -1095,15 +1095,16 @@ public class MediaControllerListenerTest { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(initialCurrentTracksRef.get()).isEqualTo(Tracks.EMPTY); - assertThat(changedCurrentTracksFromParamRef.get()).isEqualTo(currentTracks); - assertThat(changedCurrentTracksFromGetterRef.get()).isEqualTo(currentTracks); + assertThat(changedCurrentTracksFromParamRef.get().getGroups()).hasSize(2); + assertThat(changedCurrentTracksFromGetterRef.get()) + .isEqualTo(changedCurrentTracksFromParamRef.get()); assertThat(capturedEvents).hasSize(2); assertThat(getEventsAsList(capturedEvents.get(0))).containsExactly(Player.EVENT_TRACKS_CHANGED); assertThat(getEventsAsList(capturedEvents.get(1))) .containsExactly(Player.EVENT_IS_LOADING_CHANGED); assertThat(changedCurrentTracksFromOnEvents).hasSize(2); - assertThat(changedCurrentTracksFromOnEvents.get(0)).isEqualTo(currentTracks); - assertThat(changedCurrentTracksFromOnEvents.get(1)).isEqualTo(currentTracks); + assertThat(changedCurrentTracksFromOnEvents.get(0).getGroups()).hasSize(2); + assertThat(changedCurrentTracksFromOnEvents.get(1).getGroups()).hasSize(2); // Assert that an equal instance is not re-sent over the binder. assertThat(changedCurrentTracksFromOnEvents.get(0)) .isSameInstanceAs(changedCurrentTracksFromOnEvents.get(1)); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index 0bf910b686..4a4c01db4f 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -42,6 +42,7 @@ import androidx.media3.common.IllegalSeekPositionException; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Metadata; import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; @@ -50,6 +51,7 @@ import androidx.media3.common.Rating; import androidx.media3.common.StarRating; import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.Tracks; import androidx.media3.common.VideoSize; @@ -427,7 +429,7 @@ public class MediaControllerTest { assertThat(seekForwardIncrementRef.get()).isEqualTo(seekForwardIncrementMs); assertThat(maxSeekToPreviousPositionMsRef.get()).isEqualTo(maxSeekToPreviousPositionMs); assertThat(trackSelectionParametersRef.get()).isEqualTo(trackSelectionParameters); - assertThat(currentTracksRef.get()).isEqualTo(currentTracks); + assertThat(currentTracksRef.get().getGroups()).hasSize(2); assertTimelineMediaItemsEquals(timelineRef.get(), timeline); assertThat(currentMediaItemIndexRef.get()).isEqualTo(currentMediaItemIndex); assertThat(currentMediaItemRef.get()).isEqualTo(currentMediaItem); @@ -1211,6 +1213,118 @@ public class MediaControllerTest { assertThat(mediaMetadata).isEqualTo(testMediaMetadata); } + @Test + public void getCurrentTracks_hasEqualTrackGroupsForEqualGroupsInPlayer() throws Exception { + // Include metadata in Format to ensure the track group can't be fully bundled. + Tracks initialPlayerTracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup( + new Format.Builder().setMetadata(new Metadata()).setId("1").build()), + /* adaptiveSupported= */ false, + /* trackSupport= */ new int[1], + /* trackSelected= */ new boolean[1]), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setMetadata(new Metadata()).setId("2").build()), + /* adaptiveSupported= */ false, + /* trackSupport= */ new int[1], + /* trackSelected= */ new boolean[1]))); + Tracks updatedPlayerTracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup( + new Format.Builder().setMetadata(new Metadata()).setId("2").build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setMetadata(new Metadata()).setId("3").build()), + /* adaptiveSupported= */ false, + /* trackSupport= */ new int[1], + /* trackSelected= */ new boolean[1]))); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setCurrentTracks(initialPlayerTracks) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + CountDownLatch trackChangedEvent = new CountDownLatch(1); + threadTestRule + .getHandler() + .postAndSync( + () -> + controller.addListener( + new Player.Listener() { + @Override + public void onTracksChanged(Tracks tracks) { + trackChangedEvent.countDown(); + } + })); + + Tracks initialControllerTracks = + threadTestRule.getHandler().postAndSync(controller::getCurrentTracks); + // Do something unrelated first to ensure tracks are correctly kept even after multiple updates. + remoteSession.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_READY); + remoteSession.getMockPlayer().notifyTracksChanged(updatedPlayerTracks); + trackChangedEvent.await(); + Tracks updatedControllerTracks = + threadTestRule.getHandler().postAndSync(controller::getCurrentTracks); + + assertThat(initialControllerTracks.getGroups()).hasSize(2); + assertThat(updatedControllerTracks.getGroups()).hasSize(2); + assertThat(initialControllerTracks.getGroups().get(1).getMediaTrackGroup()) + .isEqualTo(updatedControllerTracks.getGroups().get(0).getMediaTrackGroup()); + } + + @Test + public void getCurrentTracksAndTrackOverrides_haveEqualTrackGroupsForEqualGroupsInPlayer() + throws Exception { + // Include metadata in Format to ensure the track group can't be fully bundled. + TrackGroup playerTrackGroupForOverride = + new TrackGroup(new Format.Builder().setMetadata(new Metadata()).setId("2").build()); + Tracks playerTracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup( + new Format.Builder().setMetadata(new Metadata()).setId("1").build()), + /* adaptiveSupported= */ false, + /* trackSupport= */ new int[1], + /* trackSelected= */ new boolean[1]), + new Tracks.Group( + playerTrackGroupForOverride, + /* adaptiveSupported= */ false, + /* trackSupport= */ new int[1], + /* trackSelected= */ new boolean[1]))); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .addOverride( + new TrackSelectionOverride(playerTrackGroupForOverride, /* trackIndex= */ 0)) + .build(); + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setCurrentTracks(playerTracks) + .setTrackSelectionParameters(trackSelectionParameters) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + + Tracks controllerTracks = threadTestRule.getHandler().postAndSync(controller::getCurrentTracks); + TrackSelectionParameters controllerTrackSelectionParameters = + threadTestRule.getHandler().postAndSync(controller::getTrackSelectionParameters); + + TrackGroup controllerTrackGroup = controllerTracks.getGroups().get(1).getMediaTrackGroup(); + assertThat(controllerTrackSelectionParameters.overrides) + .containsExactly( + controllerTrackGroup, + new TrackSelectionOverride(controllerTrackGroup, /* trackIndex= */ 0)); + } + @Test public void setMediaItems_setLessMediaItemsThanCurrentMediaItemIndex_masksCurrentMediaItemIndexAndStateCorrectly() diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java index c4def47aad..172f6a8dfa 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionAndControllerTest.java @@ -24,13 +24,21 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.os.Handler; import android.os.Looper; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.Metadata; +import androidx.media3.common.MimeTypes; import androidx.media3.common.Player; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackSelectionOverride; +import androidx.media3.common.Tracks; import androidx.media3.common.util.Util; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; @@ -248,4 +256,58 @@ public class MediaSessionAndControllerTest { player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); } + + @Test + public void setTrackSelectionParameters_withOverrides_matchesExpectedTrackGroupInPlayer() + throws Exception { + MockPlayer player = + new MockPlayer.Builder().setApplicationLooper(Looper.getMainLooper()).build(); + // Intentionally add metadata to the format as this can't be bundled. + Tracks.Group trackGroupInPlayer = + new Tracks.Group( + new TrackGroup( + new Format.Builder() + .setId("0") + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setMetadata(new Metadata()) + .build(), + new Format.Builder() + .setId("1") + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setMetadata(new Metadata()) + .build()), + /* adaptiveSupported= */ false, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED, C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true, false}); + player.currentTracks = new Tracks(ImmutableList.of(trackGroupInPlayer)); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setId(TAG).build()); + MediaController controller = controllerTestRule.createController(session.getToken()); + + threadTestRule + .getHandler() + .postAndSync( + () -> + controller.setTrackSelectionParameters( + controller + .getTrackSelectionParameters() + .buildUpon() + .setOverrideForType( + new TrackSelectionOverride( + controller + .getCurrentTracks() + .getGroups() + .get(0) + .getMediaTrackGroup(), + /* trackIndex= */ 1)) + .build())); + player.awaitMethodCalled(MockPlayer.METHOD_SET_TRACK_SELECTION_PARAMETERS, TIMEOUT_MS); + + assertThat(player.trackSelectionParameters.overrides) + .containsExactly( + trackGroupInPlayer.getMediaTrackGroup(), + new TrackSelectionOverride( + trackGroupInPlayer.getMediaTrackGroup(), /* trackIndex= */ 1)); + } } From d5a81c5c4100298c96bf10734e6bd034bc19ff5b Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 14 Apr 2023 17:25:49 +0100 Subject: [PATCH 30/35] Use `@link` instead of `@value` Dackka doesn't support `@value` PiperOrigin-RevId: 524309695 (cherry picked from commit b834e49f9ff1b3777e5b69c609b9eed83e9e6a5f) --- .../transformer/MatrixTransformationFactory.java | 2 +- .../java/androidx/media3/common/AuxEffectInfo.java | 6 +++--- .../java/androidx/media3/common/util/GlUtil.java | 6 +++--- .../audio/DefaultAudioTrackBufferSizeProvider.java | 12 ++++++------ .../androidx/media3/exoplayer/ima/ImaAdsLoader.java | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java index 0f31a589c9..f4682e249c 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java @@ -28,7 +28,7 @@ import androidx.media3.effect.MatrixTransformation; */ /* package */ final class MatrixTransformationFactory { /** - * Returns a {@link MatrixTransformation} that rescales the frames over the first {@value + * Returns a {@link MatrixTransformation} that rescales the frames over the first {@link * #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases * linearly in size from a single point to filling the full output frame. */ diff --git a/libraries/common/src/main/java/androidx/media3/common/AuxEffectInfo.java b/libraries/common/src/main/java/androidx/media3/common/AuxEffectInfo.java index c51844ed75..a94f1afcaf 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AuxEffectInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/AuxEffectInfo.java @@ -52,12 +52,12 @@ public final class AuxEffectInfo { * Creates an instance with the given effect identifier and send level. * * @param effectId The effect identifier. This is the value returned by {@link - * AudioEffect#getId()} on the effect, or {@value #NO_AUX_EFFECT_ID} which represents no + * AudioEffect#getId()} on the effect, or {@link #NO_AUX_EFFECT_ID} which represents no * effect. This value is passed to {@link AudioTrack#attachAuxEffect(int)} on the underlying * audio track. * @param sendLevel The send level for the effect, where 0 represents no effect and a value of 1 - * is full send. If {@code effectId} is not {@value #NO_AUX_EFFECT_ID}, this value is passed - * to {@link AudioTrack#setAuxEffectSendLevel(float)} on the underlying audio track. + * is full send. If {@code effectId} is not {@link #NO_AUX_EFFECT_ID}, this value is passed to + * {@link AudioTrack#setAuxEffectSendLevel(float)} on the underlying audio track. */ public AuxEffectInfo(int effectId, float sendLevel) { this.effectId = effectId; diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index 128f708d29..216a01adc1 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -142,7 +142,7 @@ public final class GlUtil { } /** - * Returns whether creating a GL context with {@value #EXTENSION_PROTECTED_CONTENT} is possible. + * Returns whether creating a GL context with {@link #EXTENSION_PROTECTED_CONTENT} is possible. * *

    If {@code true}, the device supports a protected output path for DRM content when using GL. */ @@ -171,7 +171,7 @@ public final class GlUtil { } /** - * Returns whether the {@value #EXTENSION_SURFACELESS_CONTEXT} extension is supported. + * Returns whether the {@link #EXTENSION_SURFACELESS_CONTEXT} extension is supported. * *

    This extension allows passing {@link EGL14#EGL_NO_SURFACE} for both the write and read * surfaces in a call to {@link EGL14#eglMakeCurrent(EGLDisplay, EGLSurface, EGLSurface, @@ -187,7 +187,7 @@ public final class GlUtil { } /** - * Returns whether the {@value #EXTENSION_YUV_TARGET} extension is supported. + * Returns whether the {@link #EXTENSION_YUV_TARGET} extension is supported. * *

    This extension allows sampling raw YUV values from an external texture, which is required * for HDR. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java index ef40d2c4c1..e0f117d222 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackBufferSizeProvider.java @@ -80,7 +80,7 @@ public class DefaultAudioTrackBufferSizeProvider /** * Sets the minimum length for PCM {@link AudioTrack} buffers, in microseconds. Default is - * {@value #MIN_PCM_BUFFER_DURATION_US}. + * {@link #MIN_PCM_BUFFER_DURATION_US}. */ @CanIgnoreReturnValue public Builder setMinPcmBufferDurationUs(int minPcmBufferDurationUs) { @@ -90,7 +90,7 @@ public class DefaultAudioTrackBufferSizeProvider /** * Sets the maximum length for PCM {@link AudioTrack} buffers, in microseconds. Default is - * {@value #MAX_PCM_BUFFER_DURATION_US}. + * {@link #MAX_PCM_BUFFER_DURATION_US}. */ @CanIgnoreReturnValue public Builder setMaxPcmBufferDurationUs(int maxPcmBufferDurationUs) { @@ -100,7 +100,7 @@ public class DefaultAudioTrackBufferSizeProvider /** * Sets the multiplication factor to apply to the minimum buffer size requested. Default is - * {@value #PCM_BUFFER_MULTIPLICATION_FACTOR}. + * {@link #PCM_BUFFER_MULTIPLICATION_FACTOR}. */ @CanIgnoreReturnValue public Builder setPcmBufferMultiplicationFactor(int pcmBufferMultiplicationFactor) { @@ -110,7 +110,7 @@ public class DefaultAudioTrackBufferSizeProvider /** * Sets the length for passthrough {@link AudioTrack} buffers, in microseconds. Default is - * {@value #PASSTHROUGH_BUFFER_DURATION_US}. + * {@link #PASSTHROUGH_BUFFER_DURATION_US}. */ @CanIgnoreReturnValue public Builder setPassthroughBufferDurationUs(int passthroughBufferDurationUs) { @@ -119,7 +119,7 @@ public class DefaultAudioTrackBufferSizeProvider } /** - * The length for offload {@link AudioTrack} buffers, in microseconds. Default is {@value + * The length for offload {@link AudioTrack} buffers, in microseconds. Default is {@link * #OFFLOAD_BUFFER_DURATION_US}. */ @CanIgnoreReturnValue @@ -130,7 +130,7 @@ public class DefaultAudioTrackBufferSizeProvider /** * Sets the multiplication factor to apply to the passthrough buffer for AC3 to avoid underruns - * on some devices (e.g., Broadcom 7271). Default is {@value #AC3_BUFFER_MULTIPLICATION_FACTOR}. + * on some devices (e.g., Broadcom 7271). Default is {@link #AC3_BUFFER_MULTIPLICATION_FACTOR}. */ @CanIgnoreReturnValue public Builder setAc3BufferMultiplicationFactor(int ac3BufferMultiplicationFactor) { diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java index f90654a290..5ce9df28ca 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaAdsLoader.java @@ -273,7 +273,7 @@ public final class ImaAdsLoader implements AdsLoader { /** * Sets the duration in milliseconds for which the player must buffer while preloading an ad * group before that ad group is skipped and marked as having failed to load. Pass {@link - * C#TIME_UNSET} if there should be no such timeout. The default value is {@value + * C#TIME_UNSET} if there should be no such timeout. The default value is {@link * #DEFAULT_AD_PRELOAD_TIMEOUT_MS} ms. * *

    The purpose of this timeout is to avoid playback getting stuck in the unexpected case that From d784857f1f8c2ad106704351d9f6e7de081545bf Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 17 Apr 2023 11:02:48 +0100 Subject: [PATCH 31/35] Document style guide and google-java-format in CONTRIBUTING.md Prompted by discussion on this commit: https://github.com/google/ExoPlayer/commit/e8117496e0e8bb0761a4d3f387786be9a23c3002 #minor-release PiperOrigin-RevId: 524795901 (cherry picked from commit f799766db53f42c89d4de3ac5a4ad21f9ca411e9) --- CONTRIBUTING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87bf328019..5908bd201c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,21 @@ We will also consider high quality pull requests. These should merge into the `main` branch. Before a pull request can be accepted you must submit a Contributor License Agreement, as described below. +### Code style + +We follow the +[Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) +and use [`google-java-format`](https://github.com/google/google-java-format) to +automatically reformat the code. Please consider auto-formatting your changes +before opening a PR (we will otherwise do this ourselves before merging). You +can use the various IDE integrations available, or bulk-reformat all the changes +you made on top of `main` using +[`google-java-format-diff.py`](https://github.com/google/google-java-format/blob/master/scripts/google-java-format-diff.py): + +```shell +$ git diff -U0 main... | google-java-format-diff.py -p1 -i +``` + ## Contributor license agreement Contributions to any Google project must be accompanied by a Contributor From 8c5c58ec7e22ac51d24b4addbe3967c0f15a72e0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 17 Apr 2023 15:09:56 +0100 Subject: [PATCH 32/35] Remove unnecessary check for currentMediaItem in legacy session stub This check was a leftover from when the metadata was generated from the MediaItem only. Since we moved to the actual MediaMetadata fields, the check is completely unnecessary and prevents accessing metadata when the GET_CURRENT_MEDIA_ITEM command is not available. PiperOrigin-RevId: 524837587 (cherry picked from commit e0bb23d4634421ede8827948a3ddae9c5d4a9089) --- .../session/MediaSessionLegacyStub.java | 5 -- ...lerCompatCallbackWithMediaSessionTest.java | 54 ++++++++++++++++++- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index fe8f91832e..4872383ebb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1321,11 +1321,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; lastMediaMetadata = newMediaMetadata; lastDurationMs = newDurationMs; - if (currentMediaItem == null) { - setMetadata(sessionCompat, /* metadataCompat= */ null); - return; - } - @Nullable Bitmap artworkBitmap = null; ListenableFuture bitmapFuture = sessionImpl.getBitmapLoader().loadBitmapFromMetadata(newMediaMetadata); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index bd85cf9338..16fec985e2 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -1085,8 +1085,9 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { } @Test - public void onMediaMetadataChanged_updatesLegacyMetadata_correctModelConversion() - throws Exception { + public void + onMediaMetadataChanged_withGetMetadataAndGetCurrentMediaItemCommand_updatesLegacyMetadata() + throws Exception { int testItemIndex = 3; String testDisplayTitle = "displayTitle"; long testDurationMs = 30_000; @@ -1100,6 +1101,12 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { .setMediaId(testMediaItems.get(testItemIndex).mediaId) .setMediaMetadata(testMediaMetadata) .build()); + session + .getMockPlayer() + .notifyAvailableCommandsChanged( + new Player.Commands.Builder() + .addAll(Player.COMMAND_GET_METADATA, Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .build()); session.getMockPlayer().setTimeline(new PlaylistTimeline(testMediaItems)); session.getMockPlayer().setCurrentMediaItemIndex(testItemIndex); session.getMockPlayer().setDuration(testDurationMs); @@ -1131,6 +1138,49 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { assertThat(getterMetadataCompat.getString(METADATA_KEY_MEDIA_ID)).isEqualTo(testCurrentMediaId); } + @Test + public void onMediaMetadataChanged_withGetMetadataCommandOnly_updatesLegacyMetadata() + throws Exception { + int testItemIndex = 3; + String testDisplayTitle = "displayTitle"; + List testMediaItems = MediaTestUtils.createMediaItems(/* size= */ 5); + MediaMetadata testMediaMetadata = + new MediaMetadata.Builder().setTitle(testDisplayTitle).build(); + testMediaItems.set( + testItemIndex, + new MediaItem.Builder() + .setMediaId(testMediaItems.get(testItemIndex).mediaId) + .setMediaMetadata(testMediaMetadata) + .build()); + session + .getMockPlayer() + .notifyAvailableCommandsChanged( + new Player.Commands.Builder().add(Player.COMMAND_GET_METADATA).build()); + session.getMockPlayer().setTimeline(new PlaylistTimeline(testMediaItems)); + session.getMockPlayer().setCurrentMediaItemIndex(testItemIndex); + AtomicReference metadataRef = new AtomicReference<>(); + CountDownLatch latchForMetadata = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + metadataRef.set(metadata); + latchForMetadata.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + session.getMockPlayer().notifyMediaMetadataChanged(testMediaMetadata); + + assertThat(latchForMetadata.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + MediaMetadataCompat parameterMetadataCompat = metadataRef.get(); + MediaMetadataCompat getterMetadataCompat = controllerCompat.getMetadata(); + assertThat(parameterMetadataCompat.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + .isEqualTo(testDisplayTitle); + assertThat(getterMetadataCompat.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)) + .isEqualTo(testDisplayTitle); + } + @Test public void playlistChange() throws Exception { AtomicReference> queueRef = new AtomicReference<>(); From fa972d9c1f80d8b44517f534f0a8c95c3b94c8b3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 17 Apr 2023 16:08:37 +0100 Subject: [PATCH 33/35] Fix thread access when creating notifications for media sessions The sessions may have different application threads for their players, and the service with its notification provider runs on the main thread. To ensure everything runs on the correct thread, this change labels methods where needed and fixes thread access in some places. Issue: androidx/media#318 PiperOrigin-RevId: 524849598 (cherry picked from commit ffa3743069408cf794cd48e8608df749e09d719e) --- libraries/session/build.gradle | 2 + .../DefaultMediaNotificationProvider.java | 6 +- .../media3/session/MediaNotification.java | 16 +- .../session/MediaNotificationManager.java | 142 ++++++++++------- .../androidx/media3/session/MediaSession.java | 18 ++- .../media3/session/MediaSessionImpl.java | 24 ++- .../media3/session/MediaSessionService.java | 83 ++++++---- .../DefaultMediaNotificationProviderTest.java | 2 + .../session/MediaSessionServiceTest.java | 144 ++++++++++++++++++ libraries/test_utils/build.gradle | 1 + 10 files changed, 341 insertions(+), 97 deletions(-) create mode 100644 libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java diff --git a/libraries/session/build.gradle b/libraries/session/build.gradle index 74e341395f..860137f376 100644 --- a/libraries/session/build.gradle +++ b/libraries/session/build.gradle @@ -43,6 +43,8 @@ dependencies { androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion testImplementation project(modulePrefix + 'test-utils') + testImplementation project(modulePrefix + 'test-utils-robolectric') + testImplementation project(modulePrefix + 'lib-exoplayer') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index b6c487fcd2..2cf9f15305 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -33,8 +33,6 @@ import android.app.NotificationManager; import android.content.Context; import android.graphics.Bitmap; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import androidx.annotation.DoNotInline; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; @@ -245,7 +243,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi private final String channelId; @StringRes private final int channelNameResourceId; private final NotificationManager notificationManager; - private final Handler mainHandler; private @MonotonicNonNull OnBitmapLoadedFutureCallback pendingOnBitmapLoadedFutureCallback; @DrawableRes private int smallIconResourceId; @@ -278,7 +275,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi notificationManager = checkStateNotNull( (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); - mainHandler = new Handler(Looper.getMainLooper()); smallIconResourceId = R.drawable.media3_notification_small_icon; } @@ -346,7 +342,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi pendingOnBitmapLoadedFutureCallback, // This callback must be executed on the next looper iteration, after this method has // returned a media notification. - mainHandler::post); + mediaSession.getImpl().getApplicationHandler()::post); } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java index fae9ae1fee..e9ebd7bd5c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java @@ -35,6 +35,10 @@ public final class MediaNotification { /** * Creates {@linkplain NotificationCompat.Action actions} and {@linkplain PendingIntent pending * intents} for notifications. + * + *

    All methods will be called on the {@link Player#getApplicationLooper() application thread} + * of the {@link Player} associated with the {@link MediaSession} the notification is provided + * for. */ @UnstableApi public interface ActionFactory { @@ -109,10 +113,20 @@ public final class MediaNotification { * *

    The provider is required to create a {@linkplain androidx.core.app.NotificationChannelCompat * notification channel}, which is required to show notification for {@code SDK_INT >= 26}. + * + *

    All methods will be called on the {@link Player#getApplicationLooper() application thread} + * of the {@link Player} associated with the {@link MediaSession} the notification is provided + * for. */ @UnstableApi public interface Provider { - /** Receives updates for a notification. */ + /** + * Receives updates for a notification. + * + *

    All methods will be called on the {@link Player#getApplicationLooper() application thread} + * of the {@link Player} associated with the {@link MediaSession} the notification is provided + * for. + */ interface Callback { /** * Called when a {@link MediaNotification} is changed. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index 30afcb411b..a65bbf2837 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -50,6 +50,8 @@ import java.util.concurrent.TimeoutException; /** * Manages media notifications for a {@link MediaSessionService} and sets the service as * foreground/background according to the player state. + * + *

    All methods must be called on the main thread. */ /* package */ final class MediaNotificationManager { @@ -96,11 +98,12 @@ import java.util.concurrent.TimeoutException; .setListener(listener) .setApplicationLooper(Looper.getMainLooper()) .buildAsync(); + controllerMap.put(session, controllerFuture); controllerFuture.addListener( () -> { try { MediaController controller = controllerFuture.get(/* time= */ 0, TimeUnit.MILLISECONDS); - listener.onConnected(); + listener.onConnected(shouldShowNotification(session)); controller.addListener(listener); } catch (CancellationException | ExecutionException @@ -111,7 +114,6 @@ import java.util.concurrent.TimeoutException; } }, mainExecutor); - controllerMap.put(session, controllerFuture); } public void removeSession(MediaSession session) { @@ -123,46 +125,19 @@ import java.util.concurrent.TimeoutException; } public void onCustomAction(MediaSession session, String action, Bundle extras) { - @Nullable ListenableFuture controllerFuture = controllerMap.get(session); - if (controllerFuture == null) { + @Nullable MediaController mediaController = getConnectedControllerForSession(session); + if (mediaController == null) { return; } - try { - MediaController mediaController = checkStateNotNull(Futures.getDone(controllerFuture)); - if (!mediaNotificationProvider.handleCustomCommand(session, action, extras)) { - @Nullable SessionCommand customCommand = null; - for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) { - if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM - && command.customAction.equals(action)) { - customCommand = command; - break; + // Let the notification provider handle the command first before forwarding it directly. + Util.postOrRun( + new Handler(session.getPlayer().getApplicationLooper()), + () -> { + if (!mediaNotificationProvider.handleCustomCommand(session, action, extras)) { + mainExecutor.execute( + () -> sendCustomCommandIfCommandIsAvailable(mediaController, action)); } - } - if (customCommand != null - && mediaController.getAvailableSessionCommands().contains(customCommand)) { - ListenableFuture future = - mediaController.sendCustomCommand(customCommand, Bundle.EMPTY); - Futures.addCallback( - future, - new FutureCallback() { - @Override - public void onSuccess(SessionResult result) { - // Do nothing. - } - - @Override - public void onFailure(Throwable t) { - Log.w( - TAG, "custom command " + action + " produced an error: " + t.getMessage(), t); - } - }, - MoreExecutors.directExecutor()); - } - } - } catch (ExecutionException e) { - // We should never reach this. - throw new IllegalStateException(e); - } + }); } /** @@ -178,27 +153,42 @@ import java.util.concurrent.TimeoutException; } int notificationSequence = ++totalNotificationCount; + ImmutableList customLayout = checkStateNotNull(customLayoutMap.get(session)); MediaNotification.Provider.Callback callback = notification -> mainExecutor.execute( () -> onNotificationUpdated(notificationSequence, session, notification)); - - MediaNotification mediaNotification = - this.mediaNotificationProvider.createNotification( - session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback); - updateNotificationInternal(session, mediaNotification, startInForegroundRequired); + Util.postOrRun( + new Handler(session.getPlayer().getApplicationLooper()), + () -> { + MediaNotification mediaNotification = + this.mediaNotificationProvider.createNotification( + session, customLayout, actionFactory, callback); + mainExecutor.execute( + () -> + updateNotificationInternal( + session, mediaNotification, startInForegroundRequired)); + }); } public boolean isStartedInForeground() { return startedInForeground; } + /* package */ boolean shouldRunInForeground( + MediaSession session, boolean startInForegroundWhenPaused) { + @Nullable MediaController controller = getConnectedControllerForSession(session); + return controller != null + && (controller.getPlayWhenReady() || startInForegroundWhenPaused) + && (controller.getPlaybackState() == Player.STATE_READY + || controller.getPlaybackState() == Player.STATE_BUFFERING); + } + private void onNotificationUpdated( int notificationSequence, MediaSession session, MediaNotification mediaNotification) { if (notificationSequence == totalNotificationCount) { boolean startInForegroundRequired = - MediaSessionService.shouldRunInForeground( - session, /* startInForegroundWhenPaused= */ false); + shouldRunInForeground(session, /* startInForegroundWhenPaused= */ false); updateNotificationInternal(session, mediaNotification, startInForegroundRequired); } } @@ -236,8 +226,7 @@ import java.util.concurrent.TimeoutException; private void maybeStopForegroundService(boolean removeNotifications) { List sessions = mediaSessionService.getSessions(); for (int i = 0; i < sessions.size(); i++) { - if (MediaSessionService.shouldRunInForeground( - sessions.get(i), /* startInForegroundWhenPaused= */ false)) { + if (shouldRunInForeground(sessions.get(i), /* startInForegroundWhenPaused= */ false)) { return; } } @@ -251,9 +240,56 @@ import java.util.concurrent.TimeoutException; } } - private static boolean shouldShowNotification(MediaSession session) { - Player player = session.getPlayer(); - return !player.getCurrentTimeline().isEmpty() && player.getPlaybackState() != Player.STATE_IDLE; + private boolean shouldShowNotification(MediaSession session) { + MediaController controller = getConnectedControllerForSession(session); + return controller != null + && !controller.getCurrentTimeline().isEmpty() + && controller.getPlaybackState() != Player.STATE_IDLE; + } + + @Nullable + private MediaController getConnectedControllerForSession(MediaSession session) { + @Nullable ListenableFuture controllerFuture = controllerMap.get(session); + if (controllerFuture == null) { + return null; + } + try { + return Futures.getDone(controllerFuture); + } catch (ExecutionException exception) { + // We should never reach this. + throw new IllegalStateException(exception); + } + } + + private void sendCustomCommandIfCommandIsAvailable( + MediaController mediaController, String action) { + @Nullable SessionCommand customCommand = null; + for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) { + if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM + && command.customAction.equals(action)) { + customCommand = command; + break; + } + } + if (customCommand != null + && mediaController.getAvailableSessionCommands().contains(customCommand)) { + ListenableFuture future = + mediaController.sendCustomCommand(customCommand, Bundle.EMPTY); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(SessionResult result) { + // Do nothing. + } + + @Override + public void onFailure(Throwable t) { + Log.w(TAG, "custom command " + action + " produced an error: " + t.getMessage(), t); + } + }, + MoreExecutors.directExecutor()); + } } private static final class MediaControllerListener @@ -271,8 +307,8 @@ import java.util.concurrent.TimeoutException; this.customLayoutMap = customLayoutMap; } - public void onConnected() { - if (shouldShowNotification(session)) { + public void onConnected(boolean shouldShowNotification) { + if (shouldShowNotification) { mediaSessionService.onUpdateNotificationInternal( session, /* startInForegroundWhenPaused= */ false); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 33e9af7920..475e13020e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -899,12 +899,20 @@ public class MediaSession { impl.setSessionPositionUpdateDelayMsOnHandler(updateDelayMs); } - /** Sets the {@linkplain Listener listener}. */ + /** + * Sets the {@linkplain Listener listener}. + * + *

    This method must be called on the main thread. + */ /* package */ void setListener(Listener listener) { impl.setMediaSessionListener(listener); } - /** Clears the {@linkplain Listener listener}. */ + /** + * Clears the {@linkplain Listener listener}. + * + *

    This method must be called on the main thread. + */ /* package */ void clearListener() { impl.clearMediaSessionListener(); } @@ -1435,7 +1443,11 @@ public class MediaSession { default void onRenderedFirstFrame(int seq) throws RemoteException {} } - /** Listener for media session events */ + /** + * Listener for media session events. + * + *

    All methods must be called on the main thread. + */ /* package */ interface Listener { /** diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 95082214a4..ee865ba11d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -68,9 +68,11 @@ import androidx.media3.session.SequencedFutureManager.SequencedFuture; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; import org.checkerframework.checker.initialization.qual.Initialized; /* package */ class MediaSessionImpl { @@ -112,8 +114,10 @@ import org.checkerframework.checker.initialization.qual.Initialized; private final Handler applicationHandler; private final BitmapLoader bitmapLoader; private final Runnable periodicSessionPositionInfoUpdateRunnable; + private final Handler mainHandler; @Nullable private PlayerListener playerListener; + @Nullable private MediaSession.Listener mediaSessionListener; private PlayerInfo playerInfo; @@ -148,6 +152,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; sessionStub = new MediaSessionStub(thisRef); this.sessionActivity = sessionActivity; + mainHandler = new Handler(Looper.getMainLooper()); applicationHandler = new Handler(player.getApplicationLooper()); this.callback = callback; this.bitmapLoader = bitmapLoader; @@ -545,12 +550,25 @@ import org.checkerframework.checker.initialization.qual.Initialized; } /* package */ void onNotificationRefreshRequired() { - if (this.mediaSessionListener != null) { - this.mediaSessionListener.onNotificationRefreshRequired(instance); - } + postOrRun( + mainHandler, + () -> { + if (this.mediaSessionListener != null) { + this.mediaSessionListener.onNotificationRefreshRequired(instance); + } + }); } /* package */ boolean onPlayRequested() { + if (Looper.myLooper() != Looper.getMainLooper()) { + SettableFuture playRequested = SettableFuture.create(); + mainHandler.post(() -> playRequested.set(onPlayRequested())); + try { + return playRequested.get(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + } if (this.mediaSessionListener != null) { return this.mediaSessionListener.onPlayRequested(instance); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index 30b76997f5..69f41b2e22 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -40,7 +40,6 @@ import androidx.annotation.RequiresApi; import androidx.collection.ArrayMap; import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaSessionManager; -import androidx.media3.common.Player; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -183,7 +182,6 @@ public abstract class MediaSessionService extends Service { @Nullable private Listener listener; - @GuardedBy("lock") private boolean defaultMethodCalled; /** Creates a service. */ @@ -198,6 +196,8 @@ public abstract class MediaSessionService extends Service { * Called when the service is created. * *

    Override this method if you need your own initialization. + * + *

    This method will be called on the main thread. */ @CallSuper @Override @@ -234,7 +234,7 @@ public abstract class MediaSessionService extends Service { *

    For those special cases, the values returned by {@link ControllerInfo#getUid()} and {@link * ControllerInfo#getConnectionHints()} have no meaning. * - *

    This method is always called on the main thread. + *

    This method will be called on the main thread. * * @param controllerInfo The information of the controller that is trying to connect. * @return A {@link MediaSession} for the controller, or {@code null} to reject the connection. @@ -251,6 +251,8 @@ public abstract class MediaSessionService extends Service { *

    The added session will be removed automatically {@linkplain MediaSession#release() when the * session is released}. * + *

    This method can be called from any thread. + * * @param session A session to be added. * @see #removeSession(MediaSession) * @see #getSessions() @@ -268,8 +270,12 @@ public abstract class MediaSessionService extends Service { // Session has returned for the first time. Register callbacks. // TODO(b/191644474): Check whether the session is registered to multiple services. MediaNotificationManager notificationManager = getMediaNotificationManager(); - postOrRun(mainHandler, () -> notificationManager.addSession(session)); - session.setListener(new MediaSessionListener()); + postOrRun( + mainHandler, + () -> { + notificationManager.addSession(session); + session.setListener(new MediaSessionListener()); + }); } } @@ -277,6 +283,8 @@ public abstract class MediaSessionService extends Service { * Removes a {@link MediaSession} from this service. This is not necessary for most media apps. * See Supporting Multiple Sessions for details. * + *

    This method can be called from any thread. + * * @param session A session to be removed. * @see #addSession(MediaSession) * @see #getSessions() @@ -288,13 +296,19 @@ public abstract class MediaSessionService extends Service { sessions.remove(session.getId()); } MediaNotificationManager notificationManager = getMediaNotificationManager(); - postOrRun(mainHandler, () -> notificationManager.removeSession(session)); - session.clearListener(); + postOrRun( + mainHandler, + () -> { + notificationManager.removeSession(session); + session.clearListener(); + }); } /** * Returns the list of {@linkplain MediaSession sessions} that you've added to this service via * {@link #addSession} or {@link #onGetSession(ControllerInfo)}. + * + *

    This method can be called from any thread. */ public final List getSessions() { synchronized (lock) { @@ -305,6 +319,8 @@ public abstract class MediaSessionService extends Service { /** * Returns whether {@code session} has been added to this service via {@link #addSession} or * {@link #onGetSession(ControllerInfo)}. + * + *

    This method can be called from any thread. */ public final boolean isSessionAdded(MediaSession session) { synchronized (lock) { @@ -312,7 +328,11 @@ public abstract class MediaSessionService extends Service { } } - /** Sets the {@linkplain Listener listener}. */ + /** + * Sets the {@linkplain Listener listener}. + * + *

    This method can be called from any thread. + */ @UnstableApi public final void setListener(Listener listener) { synchronized (lock) { @@ -320,7 +340,11 @@ public abstract class MediaSessionService extends Service { } } - /** Clears the {@linkplain Listener listener}. */ + /** + * Clears the {@linkplain Listener listener}. + * + *

    This method can be called from any thread. + */ @UnstableApi public final void clearListener() { synchronized (lock) { @@ -335,6 +359,8 @@ public abstract class MediaSessionService extends Service { * controllers}. In this case, the intent will have the action {@link #SERVICE_INTERFACE}. * Override this method if this service also needs to handle actions other than {@link * #SERVICE_INTERFACE}. + * + *

    This method will be called on the main thread. */ @CallSuper @Override @@ -378,6 +404,8 @@ public abstract class MediaSessionService extends Service { *

    The default implementation handles the incoming media button events. In this case, the * intent will have the action {@link Intent#ACTION_MEDIA_BUTTON}. Override this method if this * service also needs to handle actions other than {@link Intent#ACTION_MEDIA_BUTTON}. + * + *

    This method will be called on the main thread. */ @CallSuper @Override @@ -417,6 +445,8 @@ public abstract class MediaSessionService extends Service { * Called when the service is no longer used and is being removed. * *

    Override this method if you need your own clean up. + * + *

    This method will be called on the main thread. */ @CallSuper @Override @@ -456,7 +486,7 @@ public abstract class MediaSessionService extends Service { * @param session A session that needs notification update. */ public void onUpdateNotification(MediaSession session) { - setDefaultMethodCalled(true); + defaultMethodCalled = true; } /** @@ -483,13 +513,15 @@ public abstract class MediaSessionService extends Service { *

    Apps targeting {@code SDK_INT >= 28} must request the permission, {@link * android.Manifest.permission#FOREGROUND_SERVICE}. * + *

    This method will be called on the main thread. + * * @param session A session that needs notification update. * @param startInForegroundRequired Whether the service is required to start in the foreground. */ @UnstableApi public void onUpdateNotification(MediaSession session, boolean startInForegroundRequired) { onUpdateNotification(session); - if (isDefaultMethodCalled()) { + if (defaultMethodCalled) { getMediaNotificationManager().updateNotification(session, startInForegroundRequired); } } @@ -498,6 +530,8 @@ public abstract class MediaSessionService extends Service { * Sets the {@link MediaNotification.Provider} to customize notifications. * *

    This should be called before {@link #onCreate()} returns. + * + *

    This method can be called from any thread. */ @UnstableApi protected final void setMediaNotificationProvider( @@ -514,11 +548,16 @@ public abstract class MediaSessionService extends Service { } } + /** + * Triggers notification update and handles {@code ForegroundServiceStartNotAllowedException}. + * + *

    This method will be called on the main thread. + */ /* package */ boolean onUpdateNotificationInternal( MediaSession session, boolean startInForegroundWhenPaused) { try { boolean startInForegroundRequired = - shouldRunInForeground(session, startInForegroundWhenPaused); + getMediaNotificationManager().shouldRunInForeground(session, startInForegroundWhenPaused); onUpdateNotification(session, startInForegroundRequired); } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) { if ((Util.SDK_INT >= 31) && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) { @@ -531,14 +570,6 @@ public abstract class MediaSessionService extends Service { return true; } - /* package */ static boolean shouldRunInForeground( - MediaSession session, boolean startInForegroundWhenPaused) { - Player player = session.getPlayer(); - return (player.getPlayWhenReady() || startInForegroundWhenPaused) - && (player.getPlaybackState() == Player.STATE_READY - || player.getPlaybackState() == Player.STATE_BUFFERING); - } - private MediaNotificationManager getMediaNotificationManager() { synchronized (lock) { if (mediaNotificationManager == null) { @@ -570,18 +601,6 @@ public abstract class MediaSessionService extends Service { } } - private boolean isDefaultMethodCalled() { - synchronized (lock) { - return this.defaultMethodCalled; - } - } - - private void setDefaultMethodCalled(boolean defaultMethodCalled) { - synchronized (lock) { - this.defaultMethodCalled = defaultMethodCalled; - } - } - @RequiresApi(31) private void onForegroundServiceStartNotAllowedException() { mainHandler.post( diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index cd159ea0be..591dbaa653 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -36,6 +36,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -785,6 +786,7 @@ public class DefaultMediaNotificationProviderTest { when(mockMediaSession.getPlayer()).thenReturn(mockPlayer); MediaSessionImpl mockMediaSessionImpl = mock(MediaSessionImpl.class); when(mockMediaSession.getImpl()).thenReturn(mockMediaSessionImpl); + when(mockMediaSessionImpl.getApplicationHandler()).thenReturn(new Handler(Looper.myLooper())); when(mockMediaSessionImpl.getUri()).thenReturn(Uri.parse("https://example.test")); return mockMediaSession; } diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java new file mode 100644 index 0000000000..fbac60257a --- /dev/null +++ b/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2023 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 androidx.media3.session; + +import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; +import static com.google.common.truth.Truth8.assertThat; +import static java.util.Arrays.stream; + +import android.app.NotificationManager; +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.service.notification.StatusBarNotification; +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.test.utils.TestExoPlayerBuilder; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.android.controller.ServiceController; +import org.robolectric.shadows.ShadowLooper; + +@RunWith(AndroidJUnit4.class) +public class MediaSessionServiceTest { + + @Test + public void service_multipleSessionsOnMainThread_createsNotificationForEachSession() { + Context context = ApplicationProvider.getApplicationContext(); + ExoPlayer player1 = new TestExoPlayerBuilder(context).build(); + ExoPlayer player2 = new TestExoPlayerBuilder(context).build(); + MediaSession session1 = new MediaSession.Builder(context, player1).setId("1").build(); + MediaSession session2 = new MediaSession.Builder(context, player2).setId("2").build(); + ServiceController serviceController = Robolectric.buildService(TestService.class); + TestService service = serviceController.create().get(); + service.setMediaNotificationProvider( + new DefaultMediaNotificationProvider( + service, + session -> 2000 + Integer.parseInt(session.getId()), + DefaultMediaNotificationProvider.DEFAULT_CHANNEL_ID, + DefaultMediaNotificationProvider.DEFAULT_CHANNEL_NAME_RESOURCE_ID)); + + service.addSession(session1); + service.addSession(session2); + // Start the players so that we also create notifications for them. + player1.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); + player1.prepare(); + player1.play(); + player2.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); + player2.prepare(); + player2.play(); + ShadowLooper.idleMainLooper(); + + NotificationManager notificationService = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + assertThat( + stream(notificationService.getActiveNotifications()).map(StatusBarNotification::getId)) + .containsExactly(2001, 2002); + + serviceController.destroy(); + session1.release(); + session2.release(); + player1.release(); + player2.release(); + } + + @Test + public void service_multipleSessionsOnDifferentThreads_createsNotificationForEachSession() + throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + HandlerThread thread1 = new HandlerThread("player1"); + HandlerThread thread2 = new HandlerThread("player2"); + thread1.start(); + thread2.start(); + ExoPlayer player1 = new TestExoPlayerBuilder(context).setLooper(thread1.getLooper()).build(); + ExoPlayer player2 = new TestExoPlayerBuilder(context).setLooper(thread2.getLooper()).build(); + MediaSession session1 = new MediaSession.Builder(context, player1).setId("1").build(); + MediaSession session2 = new MediaSession.Builder(context, player2).setId("2").build(); + ServiceController serviceController = Robolectric.buildService(TestService.class); + TestService service = serviceController.create().get(); + service.setMediaNotificationProvider( + new DefaultMediaNotificationProvider( + service, + session -> 2000 + Integer.parseInt(session.getId()), + DefaultMediaNotificationProvider.DEFAULT_CHANNEL_ID, + DefaultMediaNotificationProvider.DEFAULT_CHANNEL_NAME_RESOURCE_ID)); + NotificationManager notificationService = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + service.addSession(session1); + service.addSession(session2); + // Start the players so that we also create notifications for them. + new Handler(thread1.getLooper()) + .post( + () -> { + player1.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); + player1.prepare(); + player1.play(); + }); + new Handler(thread2.getLooper()) + .post( + () -> { + player2.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); + player2.prepare(); + player2.play(); + }); + runMainLooperUntil(() -> notificationService.getActiveNotifications().length == 2); + + assertThat( + stream(notificationService.getActiveNotifications()).map(StatusBarNotification::getId)) + .containsExactly(2001, 2002); + + serviceController.destroy(); + session1.release(); + session2.release(); + new Handler(thread1.getLooper()).post(player1::release); + new Handler(thread2.getLooper()).post(player2::release); + thread1.quit(); + thread2.quit(); + } + + private static final class TestService extends MediaSessionService { + @Nullable + @Override + public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { + return null; // No need to support binding or pending intents for this test. + } + } +} diff --git a/libraries/test_utils/build.gradle b/libraries/test_utils/build.gradle index d3901cf175..239104ad53 100644 --- a/libraries/test_utils/build.gradle +++ b/libraries/test_utils/build.gradle @@ -20,6 +20,7 @@ dependencies { api 'androidx.test.ext:truth:' + androidxTestTruthVersion api 'junit:junit:' + junitVersion api 'com.google.truth:truth:' + truthVersion + api 'com.google.truth.extensions:truth-java8-extension:' + truthVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion From b1e3eac0f28b8ad6942a6fed7fc9f316c1915f4f Mon Sep 17 00:00:00 2001 From: rohks Date: Tue, 18 Apr 2023 18:00:11 +0100 Subject: [PATCH 34/35] Update release notes for Media3 1.0.1 / ExoPlayer 2.18.6 PiperOrigin-RevId: 525176828 (cherry picked from commit 07ff3eeeadc6d7a00ad3bf941aebe34f5ac6855a) --- RELEASENOTES.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 535e4e4f1f..acce6295ca 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,44 @@ # Release notes +### 1.0.1 (2023-04-18) + +This release corresponds to the +[ExoPlayer 2.18.6 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.6). + +* Core library: + * Reset target live stream override when seeking to default position + ([#11051](https://github.com/google/ExoPlayer/pull/11051)). + * Fix bug where empty sample streams in the media could cause playback to + be stuck. +* Session: + * Fix bug where multiple identical queue items published by a legacy + `MediaSessionCompat` result in an exception in `MediaController` + ([#290](https://github.com/androidx/media/issues/290)). + * Add missing forwarding of `MediaSession.broadcastCustomCommand` to the + legacy `MediaControllerCompat.Callback.onSessionEvent` + ([#293](https://github.com/androidx/media/issues/293)). + * Fix bug where calling `MediaSession.setPlayer` doesn't update the + available commands. + * Fix issue that `TrackSelectionOverride` instances sent from a + `MediaController` are ignored if they reference a group with + `Format.metadata` + ([#296](https://github.com/androidx/media/issues/296)). + * Fix issue where `Player.COMMAND_GET_CURRENT_MEDIA_ITEM` needs to be + available to access metadata via the legacy `MediaSessionCompat`. + * Fix issue where `MediaSession` instances on a background thread cause + crashes when used in `MediaSessionService` + ([#318](https://github.com/androidx/media/issues/318)). + * Fix issue where a media button receiver was declared by the library + without the app having intended this + ([#314](https://github.com/androidx/media/issues/314)). +* DASH: + * Fix handling of empty segment timelines + ([#11014](https://github.com/google/ExoPlayer/issues/11014)). +* RTSP: + * Retry with TCP if RTSP Setup with UDP fails with RTSP Error 461 + UnsupportedTransport + ([#11069](https://github.com/google/ExoPlayer/issues/11069)). + ### 1.0.0 (2023-03-22) This release corresponds to the From 4f0b30b8a5966bafaabd180e27019cc7ec15a8c2 Mon Sep 17 00:00:00 2001 From: rohks Date: Wed, 19 Apr 2023 12:57:48 +0100 Subject: [PATCH 35/35] Add `com.google.truth.extensions:truth-java8-extension` to JAR list PiperOrigin-RevId: 525415067 (cherry picked from commit 3788172afdf7531f7adc9deb52a2a65c292a8b01) --- missing_aar_type_workaround.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/missing_aar_type_workaround.gradle b/missing_aar_type_workaround.gradle index b0b2fe7035..5aa71111c3 100644 --- a/missing_aar_type_workaround.gradle +++ b/missing_aar_type_workaround.gradle @@ -24,6 +24,7 @@ def addMissingAarTypeToXml(xml) { "com.google.ads.interactivemedia.v3:interactivemedia", "com.google.guava:guava", "com.google.truth:truth", + "com.google.truth.extensions:truth-java8-extension", "com.squareup.okhttp3:okhttp", "com.squareup.okhttp3:mockwebserver", "org.mockito:mockito-core", @@ -77,6 +78,11 @@ def addMissingAarTypeToXml(xml) { (isProjectLibrary || aar_dependencies.contains(dependencyName)) if (!hasJar && !hasAar) { + // To look for what kind of dependency it is i.e. aar or jar type, + // please expand the External Libraries in Project view in Android Studio + // and search for your dependency inside Gradle Script dependencies. + // .aar files have @aar suffix at the end of their name, + // while .jar files have nothing. throw new IllegalStateException( dependencyName + " is not on the JAR or AAR list in missing_aar_type_workaround.gradle") }