From ab4489180d6e04b40e4c3ebad3f79ec43590ecba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 4 Dec 2024 10:54:27 +0100 Subject: [PATCH 1/6] Improve accessibility in the demo app --- .../pillarbox/demo/shared/data/DemoItem.kt | 151 ++++++++++---- .../pillarbox/demo/shared/data/Playlist.kt | 187 ++++++++++++++---- .../source/BlockedTimeRangeAssetLoader.kt | 9 +- .../shared/ui/examples/ExamplesViewModel.kt | 5 +- .../shared/ui/integrationLayer/ContentList.kt | 22 ++- .../data/ContentListSection.kt | 4 +- .../data/ContentListSections.kt | 19 +- .../src/main/res/values/strings.xml | 1 + .../srgssr/pillarbox/demo/MainNavigation.kt | 28 ++- .../demo/ui/components/ContentView.kt | 8 + .../demo/ui/components/DemoListHeaderView.kt | 17 +- .../demo/ui/components/DemoListItemView.kt | 21 +- .../demo/ui/examples/ExamplesHome.kt | 4 +- .../pillarbox/demo/ui/lists/ListsHome.kt | 34 +++- .../demo/ui/lists/ListsSubSection.kt | 25 ++- .../demo/ui/player/SimplePlayerActivity.kt | 2 +- .../ui/player/playlist/MediaItemLibrary.kt | 4 +- .../ui/player/playlist/PlaylistItemView.kt | 8 +- .../demo/ui/player/playlist/PlaylistView.kt | 42 +++- .../pillarbox/demo/ui/search/SearchHome.kt | 109 +++++++++- .../demo/ui/settings/AppSettingsView.kt | 45 ++++- .../demo/ui/showcases/ShowcasesHome.kt | 184 ++++++++--------- .../ui/showcases/layouts/ChapterShowcase.kt | 46 ++++- .../layouts/ChaptersShowcaseViewModel.kt | 7 +- .../CustomPlaybackSettingsShowcase.kt | 21 +- .../src/main/res/values/strings.xml | 6 + 26 files changed, 761 insertions(+), 248 deletions(-) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoItem.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoItem.kt index e8bd0eaa8..a7c1593f2 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoItem.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/DemoItem.kt @@ -21,12 +21,14 @@ import java.io.Serializable * @property title The title of the media * @property description The optional description of the media. * @property imageUri The optional image URI of the media. + * @property languageTag The IETF BCP47 language tag of the title and description. */ sealed class DemoItem( open val uri: String, open val title: String?, open val description: String?, open val imageUri: String?, + open val languageTag: String? = null, ) : Serializable { /** * Represents a media item playable by URL. @@ -35,6 +37,7 @@ sealed class DemoItem( * @property title The title of the media * @property description The optional description of the media. * @property imageUri The optional image URI of the media. + * @property languageTag The IETF BCP47 language tag of the title and description. * @property licenseUri The optional license URI of the media. */ data class URL( @@ -42,8 +45,9 @@ sealed class DemoItem( override val title: String? = null, override val description: String? = null, override val imageUri: String? = null, + override val languageTag: String? = null, val licenseUri: String? = null, - ) : DemoItem(uri, title, description, imageUri) { + ) : DemoItem(uri, title, description, imageUri, languageTag) { override fun toMediaItem(): MediaItem { return MediaItem.Builder() .setUri(uri) @@ -74,6 +78,7 @@ sealed class DemoItem( * @property title The title of the media * @property description The optional description of the media. * @property imageUri The optional image URI of the media. + * @property languageTag The IETF BCP47 language tag of the title and description. * @property host The host from which to load the media. * @property forceSAM Whether to use SAM instead of the IL. * @property ilLocation The optional location from which to load the media. @@ -83,10 +88,11 @@ sealed class DemoItem( override val title: String? = null, override val description: String? = null, override val imageUri: String? = null, + override val languageTag: String? = null, val host: java.net.URL = IlHost.PROD, val forceSAM: Boolean = false, val ilLocation: IlLocation? = null, - ) : DemoItem(urn, title, description, imageUri) { + ) : DemoItem(urn, title, description, imageUri, languageTag) { override fun toMediaItem(): MediaItem { return SRGMediaItem(urn) { host(host) @@ -106,7 +112,7 @@ sealed class DemoItem( */ abstract fun toMediaItem(): MediaItem - @Suppress("MaximumLineLength", "MaxLineLength", "UndocumentedPublicClass", "UndocumentedPublicProperty") + @Suppress("StringLiteralDuplication", "MaximumLineLength", "MaxLineLength", "UndocumentedPublicClass", "UndocumentedPublicProperty") companion object { @Suppress("ConstPropertyName") private const val serialVersionUID: Long = 1 @@ -115,370 +121,438 @@ sealed class DemoItem( title = "VOD - HLS", uri = "https://rts-vod-amd.akamaized.net/ww/14970442/7510ee63-05a4-3d48-8d26-1f1b3a82f6be/master.m3u8", description = "Sacha part à la rencontre d'univers atypiques", + languageTag = "fr-CH", ) val ShortOnDemandVideoHLS = URL( title = "VOD - HLS (short)", uri = "https://rts-vod-amd.akamaized.net/ww/13317145/f1d49f18-f302-37ce-866c-1c1c9b76a824/master.m3u8", description = "Des violents orages ont touché Ajaccio, chef-lieu de la Corse, jeudi", + languageTag = "fr-CH", ) val OnDemandVideoMP4 = URL( title = "VOD - MP4", uri = "https://cdn.prod.swi-services.ch/video-projects/94f5f5d1-5d53-4336-afda-9198462c45d9/localised-videos/ENG/renditions/ENG.mp4", description = "Swiss wheelchair athlete wins top award", + languageTag = "en-CH", ) val OnDemandVideoUHD = URL( title = "Brain Farm Skate Phantom Flex", uri = "https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8", description = "4K video", + languageTag = "en-CH", ) val LiveVideoHLS = URL( title = "Video livestream - HLS", uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0", description = "Couleur 3 en vidéo (live)", + languageTag = "fr-CH", ) val DvrVideoHLS = URL( title = "Video livestream with DVR - HLS", uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8", description = "Couleur 3 en vidéo (DVR)", + languageTag = "fr-CH", ) val LiveTimestampVideoHLS = URL( title = "Video livestream with DVR and timestamps - HLS", uri = "https://tagesschau.akamaized.net/hls/live/2020115/tagesschau/tagesschau_1/master.m3u8", description = "Tageschau", + languageTag = "de-CH", ) val OnDemandAudioMP3 = URL( title = "AOD - MP3", uri = "https://rts-aod-dd.akamaized.net/ww/13306839/63cc2653-8305-3894-a448-108810b553ef.mp3", description = "On en parle", + languageTag = "fr-CH", ) val LiveAudioMP3 = URL( title = "Audio livestream - MP3", uri = "https://stream.srg-ssr.ch/m/couleur3/mp3_128", description = "Couleur 3 (live)", + languageTag = "fr-CH", ) val DvrAudioHLS = URL( title = "Audio livestream - HLS", uri = "https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8", description = "Couleur 3 (DVR)", + languageTag = "fr-CH", ) val AppleBasic_4_3_HLS = URL( title = "Apple Basic 4:3", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", description = "4x3 aspect ratio, H.264 @ 30Hz", + languageTag = "en-CH", ) val AppleBasic_16_9_TS_HLS = URL( title = "Apple Basic 16:9", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8", description = "16x9 aspect ratio, H.264 @ 30Hz", + languageTag = "en-CH", ) val AppleAdvanced_16_9_TS_HLS = URL( title = "Apple Advanced 16:9 (TS)", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8", description = "16x9 aspect ratio, H.264 @ 30Hz and 60Hz, Transport stream", + languageTag = "en-CH", ) val AppleAdvanced_16_9_fMP4_HLS = URL( title = "Apple Advanced 16:9 (fMP4)", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8", description = "16x9 aspect ratio, H.264 @ 30Hz and 60Hz, Fragmented MP4", + languageTag = "en-CH", ) val AppleAdvanced_16_9_HEVC_h264_HLS = URL( title = "Apple Advanced 16:9 (HEVC/H.264)", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8", description = "16x9 aspect ratio, H.264 and HEVC @ 30Hz and 60Hz", + languageTag = "en-CH", ) val AppleAtmos = URL( title = "Apple Atmos", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8", + languageTag = "en-CH", ) val AppleWWDC_2023 = URL( title = "Apple WWDC Keynote 2023", uri = "https://events-delivery.apple.com/0105cftwpxxsfrpdwklppzjhjocakrsk/m3u8/vod_index-PQsoJoECcKHTYzphNkXohHsQWACugmET.m3u8", + languageTag = "en-CH", ) val AppleTvSample = URL( - title = "Apple tv trailer", + title = "Apple TV trailer", uri = "https://play-edge.itunes.apple.com/WebObjects/MZPlayLocal.woa/hls/subscription/playlist.m3u8?cc=CH&svcId=tvs.vds.4021&a=1522121579&isExternal=true&brandId=tvs.sbd.4000&id=518077009&l=en-GB&aec=UHD", description = "Lot of audios and subtitles choices", + languageTag = "en-CH", ) val GoogleDashH264 = URL( title = "VoD - Dash (H264)", - uri = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" + uri = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", + languageTag = "en-CH", ) val GoogleDashH264_CENC_Widewine = URL( title = "VoD - Dash Widewine cenc (H264)", uri = "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", licenseUri = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test", + languageTag = "en-CH", ) val GoogleDashH265 = URL( title = "VoD - Dash (H265)", - uri = "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" + uri = "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd", + languageTag = "en-CH", ) val GoogleDashH265_CENC_Widewine = URL( title = "VoD - Dash widewine cenc (H265)", uri = "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", licenseUri = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test", + languageTag = "en-CH", ) val OnDemandHorizontalVideo = URN( title = "Horizontal video", urn = "urn:rts:video:14827306", + languageTag = "fr-CH", ) val OnDemandSquareVideo = URN( title = "Square video", urn = "urn:rts:video:8393241", + languageTag = "en-CH", ) val OnDemandVerticalVideo = URN( title = "Vertical video", urn = "urn:rts:video:13444390", + languageTag = "en-CH", ) val TokenProtectedVideo = URN( title = "Token-protected video", urn = "urn:swisstxt:video:rts:c56ea781-99ad-40c3-8d9b-444cc5ac3aea", description = "Ski alpin, Slalom Messieurs", + languageTag = "fr-CH", ) val SuperfluouslyTokenProtectedVideo = URN( title = "Superfluously token-protected video", urn = "urn:rsi:video:15916771", description = "Telegiornale flash", + languageTag = "it-CH", ) val DrmProtectedVideo = URN( title = "DRM-protected video", urn = "urn:rts:video:13639837", description = "Top Models 8870", + languageTag = "fr-CH", ) val LiveVideo = URN( title = "Live video", urn = "urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651", description = "SRF 1", + languageTag = "de-CH", ) val DvrVideo = URN( title = "DVR video livestream", urn = "urn:rts:video:3608506", description = "RTS 1", + languageTag = "fr-CH", ) val DvrAudio = URN( title = "DVR audio livestream", urn = "urn:rts:audio:3262363", description = "Couleur 3 (DVR)", + languageTag = "fr-CH", ) val OnDemandAudio = URN( title = "On-demand audio stream", urn = "urn:srf:audio:b9706015-632f-4e24-9128-5de074d98eda", description = "Nachrichten von 08:00 Uhr - 08.03.2024", + languageTag = "de-CH", ) val Expired = URN( title = "Expired URN", urn = "urn:rts:video:13382911", description = "Content that is not available anymore", + languageTag = "en-CH", ) val Unknown = URN( title = "Unknown URN", urn = "urn:srf:video:unknown", description = "Content that does not exist", + languageTag = "en-CH", ) val BitmovinOnDemandMultipleTracks = URL( title = "Multiple subtitles and audio tracks", uri = "https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8", description = "On some devices codec may crash", + languageTag = "en-CH", ) val BitmovinOnDemand_4K_HEVC = URL( title = "4K, HEVC", - uri = "https://cdn.bitmovin.com/content/encoding_test_dash_hls/4k/hls/4k_profile/master.m3u8" + uri = "https://cdn.bitmovin.com/content/encoding_test_dash_hls/4k/hls/4k_profile/master.m3u8", + languageTag = "en-CH", ) val BitmovinOnDemandSingleAudio = URL( title = "VoD, single audio track", - uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8" + uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8", + languageTag = "en-CH", ) val BitmovinOnDemandAES128 = URL( title = "AES-128", - uri = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/m3u8s/11331.m3u8" + uri = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/m3u8s/11331.m3u8", + languageTag = "en-CH", ) val BitmovinOnDemandProgressive = URL( title = "AVC Progressive", - uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/MI201109210084_mpeg-4_hd_high_1080p25_10mbits.mp4" + uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/MI201109210084_mpeg-4_hd_high_1080p25_10mbits.mp4", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_fMP4 = URL( title = "HLS - Fragmented MP4", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8", + languageTag = "en-CH", ) val UnifiedStreamingOnDemandAlternateAudio = URL( title = "HLS - Alternate audio language", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8", + languageTag = "en-CH", ) val UnifiedStreamingOnDemandAudioOnly = URL( title = "HLS - Audio only", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8?filter=(type!=%22video%22)" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8?filter=(type!=%22video%22)", + languageTag = "en-CH", ) val UnifiedStreamingOnDemandTrickplay = URL( title = "HLS - Trickplay", - uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.m3u8" + uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.m3u8", + languageTag = "en-CH", ) val UnifiedStreamingOnDemandLimitedBandwidth = URL( title = "Limiting bandwidth use", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?max_bitrate=800000" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?max_bitrate=800000", + languageTag = "en-CH", ) val UnifiedStreamingOnDemandDynamicTrackSelection = URL( title = "Dynamic Track Selection", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?filter=%28type%3D%3D%22audio%22%26%26systemBitrate%3C100000%29%7C%7C%28type%3D%3D%22video%22%26%26systemBitrate%3C1024000%29" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?filter=%28type%3D%3D%22audio%22%26%26systemBitrate%3C100000%29%7C%7C%28type%3D%3D%22video%22%26%26systemBitrate%3C1024000%29", + languageTag = "en-CH", ) val UnifiedStreamingPureLive = URL( title = "Pure live", - uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8" + uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8", + languageTag = "en-CH", ) val UnifiedStreamingTimeshift = URL( title = "Timeshift (5 minutes)", - uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?time_shift=300" + uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?time_shift=300", + languageTag = "en-CH", ) val UnifiedStreamingLiveAudio = URL( title = "Live audio", - uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?filter=(type!=%22video%22)" + uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?filter=(type!=%22video%22)", + languageTag = "en-CH", ) val UnifiedStreamingPureLiveScte35 = URL( title = "Pure live (scte35)", - uri = "https://demo.unified-streaming.com/k8s/live/stable/scte35.isml/.m3u8" + uri = "https://demo.unified-streaming.com/k8s/live/stable/scte35.isml/.m3u8", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_fMP4_Clear = URL( title = "fMP4, clear", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-fmp4.ism/.m3u8" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-fmp4.ism/.m3u8", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_fMP4_HEVC_4K = URL( title = "fMP4, HEVC 4K", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hevc.ism/.m3u8" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hevc.ism/.m3u8", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_MP4 = URL( title = "Dash - MP4", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_FragmentedMP4 = URL( title = "Dash - Fragmented MP4", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_TrickPlay = URL( title = "Dash - TrickPlay", - uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_TiledThumbnails = URL( title = "Dash - Tiled thumbnails (live/timeline)", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_Accessibility = URL( title = "Dash - Accessibility - hard of hearing", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hoh-subs.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hoh-subs.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_Single_TTML = URL( title = "Dash - Single - fragmented TTML", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-en.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-en.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_Multiple_RFC_tags = URL( title = "Dash - Multiple - RFC 5646 language tags", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-rfc5646.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-rfc5646.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_Multiple_TTML = URL( title = "Dash - Multiple - fragmented TTML", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-ttml.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-ttml.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_AudioOnly = URL( title = "Dash - Audio only", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd?filter=(type!=%22video%22)" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd?filter=(type!=%22video%22)", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_Multiple_Audio_Codec = URL( title = "Dash - Multiple audio codecs", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-codec.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-codec.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_AlternateAudioLanguage = URL( title = "Dash - Alternate audio language", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_AccessibilityAudio = URL( title = "Dash - Accessibility - audio description", - uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-desc-aud.ism/.mpd" + uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-desc-aud.ism/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_PureLive = URL( title = "Dash - Pure live", - uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd" + uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_Timeshift = URL( title = "Dash - Timeshift (5 minutes)", - uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd?time_shift=300" + uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd?time_shift=300", + languageTag = "en-CH", ) val UnifiedStreamingOnDemand_Dash_DVB_LowLatency = URL( title = "Dash - DVB DASH low latency", - uri = "https://demo.unified-streaming.com/k8s/live/stable/live-low-latency.isml/.mpd" + uri = "https://demo.unified-streaming.com/k8s/live/stable/live-low-latency.isml/.mpd", + languageTag = "en-CH", ) val BlockedSegment = URL( title = "Blocked segment at 29:26", uri = "urn:srf:video:40ca0277-0e53-4312-83e2-4710354ff53e", imageUri = "https://ws.srf.ch/asset/image/audio/f1a1ab5d-c009-4ba1-aae0-a2be5b89edd9/EPISODE_IMAGE/1465482801.png", + languageTag = "en-CH", ) val OverlapinglockedSegments = URL( - title = "Overlaping segments", + title = "Overlapping segments", uri = "urn:srf:video:d57f5c1c-080f-49a2-864e-4a1a83e41ae1", imageUri = "https://ws.srf.ch/asset/image/audio/75c3d4a4-4357-4703-b407-2d076aa15fd7/EPISODE_IMAGE/1384985072.png", + languageTag = "en-CH", ) val MultiAudioWithAccessibility = URL( @@ -486,6 +560,7 @@ sealed class DemoItem( description = "Bonjour la Suisse (5/5) - Que du bonheur?", uri = "urn:rts:video:8806923", imageUri = "https://www.rts.ch/2017/07/28/21/11/8806915.image/16x9", + languageTag = "en-CH", ) } } diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt index 1f33d0ce0..d941e125f 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt @@ -16,10 +16,10 @@ import java.io.Serializable * * @property title * @property items - * @property description optional + * @property languageTag */ @Suppress("UndocumentedPublicProperty") -data class Playlist(val title: String, val items: List, val description: String? = null) : Serializable { +data class Playlist(val title: String, val items: List, val languageTag: String? = null) : Serializable { /** * To media item * @@ -51,56 +51,66 @@ data class Playlist(val title: String, val items: List, val descriptio uri = "https://rts-vod-amd.akamaized.net/ww/14970442/7510ee63-05a4-3d48-8d26-1f1b3a82f6be/master.m3u8", description = "VOD - HLS", imageUri = "https://www.rts.ch/2024/06/13/11/34/14970435.image/16x9", + languageTag = "fr-CH", ), DemoItem.URL( title = "Des violents orages ont touché Ajaccio, chef-lieu de la Corse, jeudi", uri = "https://rts-vod-amd.akamaized.net/ww/13317145/f1d49f18-f302-37ce-866c-1c1c9b76a824/master.m3u8", description = "VOD - HLS (short)", imageUri = "https://www.rts.ch/2022/08/18/12/38/13317144.image/16x9", + languageTag = "fr-CH", ), DemoItem.URL( title = "Swiss wheelchair athlete wins top award", uri = "https://cdn.prod.swi-services.ch/video-projects/94f5f5d1-5d53-4336-afda-9198462c45d9/localised-videos/ENG/renditions/ENG.mp4", description = "VOD - MP4 (urn:swi:video:48498670)", imageUri = "https://cdn.prod.swi-services.ch/video-delivery/images/94f5f5d1-5d53-4336-afda-9198462c45d9/_.1hAGinujJ.yERGrrGNzBGCNSxmhKZT/16x9", + languageTag = "en-CH", ), DemoItem.URL( title = "Couleur 3 en vidéo (live)", uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0", description = "Video livestream - HLS", imageUri = "https://img.rts.ch/audio/2010/image/924h3y-25865853.image?w=640&h=640", + languageTag = "fr-CH", ), DemoItem.URL( title = "Couleur 3 en vidéo (DVR)", uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8", description = "Video livestream with DVR - HLS", imageUri = "https://il.srgssr.ch/images/?imageUrl=https%3A%2F%2Fwww.rts.ch%2F2020%2F05%2F18%2F14%2F20%2F11333286.image%2F16x9&format=jpg&width=960", + languageTag = "fr-CH", ), DemoItem.URL( title = "Tagesschau", uri = "https://tagesschau.akamaized.net/hls/live/2020115/tagesschau/tagesschau_1/master.m3u8", description = "Video livestream with DVR and timestamps - HLS", imageUri = "https://images.tagesschau.de/image/89045d82-5cd5-46ad-8f91-73911add30ee/AAABh3YLLz0/AAABibBx2rU/20x9-1280/tagesschau-logo-100.jpg", + languageTag = "de-CH", ), DemoItem.URL( title = "On en parle", uri = "https://rts-aod-dd.akamaized.net/ww/13306839/63cc2653-8305-3894-a448-108810b553ef.mp3", description = "AOD - MP3", imageUri = "https://www.rts.ch/2023/09/28/17/49/11872957.image?w=624&h=351", + languageTag = "fr-CH", ), DemoItem.URL( title = "Couleur 3 (live)", uri = "https://stream.srg-ssr.ch/m/couleur3/mp3_128", description = "Audio livestream - MP3", imageUri = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=640&h=640", + languageTag = "fr-CH", ), DemoItem.URL( title = "Couleur 3 (DVR)", uri = "https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8", description = "Audio livestream - HLS", imageUri = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=640&h=640", + languageTag = "fr-CH", ), ), + languageTag = "en-CH", ) private val srgSsrStreamsUrns = Playlist( title = "SRG SSR streams (URNs)", @@ -110,34 +120,40 @@ data class Playlist(val title: String, val items: List, val descriptio urn = "urn:rts:video:3608506", description = "DVR video livestream", imageUri = "https://www.rts.ch/2023/09/06/14/43/14253742.image/16x9", + languageTag = "fr-CH", ), DemoItem.URN( title = "Couleur 3 (DVR)", urn = "urn:rts:audio:3262363", description = "DVR audio livestream", imageUri = "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9", + languageTag = "fr-CH", ), DemoItem.URN( title = "Telegiornale flash", urn = "urn:rsi:video:15916771", description = "Superfluously token-protected video", imageUri = "https://il.rsi.ch/rsi-api/resize/image/v2/WEBVISUAL/256699/", + languageTag = "it-CH", ), DemoItem.URN( title = "SRF 1", urn = "urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651", description = "Live video", imageUri = "https://ws.srf.ch/asset/image/audio/d91bbe14-55dd-458c-bc88-963462972687/EPISODE_IMAGE", + languageTag = "de-CH", ), DemoItem.URN( title = "Nachrichten von 08:00 Uhr - 08.03.2024", urn = "urn:srf:audio:b9706015-632f-4e24-9128-5de074d98eda", - description = "On-demand audio stream" + description = "On-demand audio stream", + languageTag = "de-CH", ), DemoItem.MultiAudioWithAccessibility, DemoItem.BlockedSegment, DemoItem.OverlapinglockedSegments - ) + ), + languageTag = "en-CH", ) val StoryUrns = Playlist( title = "Story urns", @@ -145,34 +161,41 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.URN( title = "Mario vs Sonic", description = "Tataki 1", - urn = "urn:rts:video:13950405" + urn = "urn:rts:video:13950405", + languageTag = "fr-CH", ), DemoItem.URN( title = "Pourquoi Beyoncé fait de la country", description = "Tataki 2", - urn = "urn:rts:video:14815579" + urn = "urn:rts:video:14815579", + languageTag = "fr-CH", ), DemoItem.URN( title = "L'île North Sentinel", description = "Tataki 3", - urn = "urn:rts:video:13795051" + urn = "urn:rts:video:13795051", + languageTag = "fr-CH", ), DemoItem.URN( title = "Mourir pour ressembler à une idole", description = "Tataki 4", - urn = "urn:rts:video:14020134" + urn = "urn:rts:video:14020134", + languageTag = "fr-CH", ), DemoItem.URN( title = "Pourquoi les gens mangent des insectes ?", description = "Tataki 5", - urn = "urn:rts:video:12631996" + urn = "urn:rts:video:12631996", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le concert de Beyoncé à Dubai", description = "Tataki 6", - urn = "urn:rts:video:13752646" + urn = "urn:rts:video:13752646", + languageTag = "fr-CH", ) - ) + ), + languageTag = "en-CH", ) private val googleStreams = Playlist( title = "Google streams", @@ -181,25 +204,30 @@ data class Playlist(val title: String, val items: List, val descriptio title = "VoD - Dash (H264)", uri = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "VoD - Dash Widewine cenc (H264)", uri = "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", licenseUri = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test", + languageTag = "en-CH", ), DemoItem.URL( title = "VoD - Dash (H265)", uri = "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "VoD - Dash widewine cenc (H265)", uri = "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", licenseUri = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test", + languageTag = "en-CH", ) - ) + ), + languageTag = "en-CH", ) private val appleStreams = Playlist( title = "Apple streams", @@ -209,52 +237,62 @@ data class Playlist(val title: String, val items: List, val descriptio uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", description = "4x3 aspect ratio, H.264 @ 30Hz", imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", + languageTag = "en-CH", ), DemoItem.URL( title = "Apple Basic 16:9", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8", description = "16x9 aspect ratio, H.264 @ 30Hz", imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", + languageTag = "en-CH", ), DemoItem.URL( title = "Apple Advanced 16:9 (TS)", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8", description = "16x9 aspect ratio, H.264 @ 30Hz and 60Hz, Transport stream", imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", + languageTag = "en-CH", ), DemoItem.URL( title = "Apple Advanced 16:9 (fMP4)", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8", description = "16x9 aspect ratio, H.264 @ 30Hz and 60Hz, Fragmented MP4", imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", + languageTag = "en-CH", ), DemoItem.URL( title = "Apple Advanced 16:9 (HEVC/H.264)", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8", description = "16x9 aspect ratio, H.264 and HEVC @ 30Hz and 60Hz", imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", + languageTag = "en-CH", ), DemoItem.URL( title = "Apple WWDC Keynote 2023", uri = "https://events-delivery.apple.com/0105cftwpxxsfrpdwklppzjhjocakrsk/m3u8/vod_index-PQsoJoECcKHTYzphNkXohHsQWACugmET.m3u8", imageUri = "https://www.apple.com/v/apple-events/home/ac/images/overview/recent-events/gallery/jun-2023__cjqmmqlyd21y_large_2x.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Apple Dolby Atmos", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8", imageUri = "https://is1-ssl.mzstatic.com/image/thumb/-6farfCY0YClFd7-z_qZbA/1000x563.webp", + languageTag = "en-CH", ), DemoItem.URL( title = "The Morning Show - My Way: Season 1", uri = "https://play-edge.itunes.apple.com/WebObjects/MZPlayLocal.woa/hls/subscription/playlist.m3u8?cc=CH&svcId=tvs.vds.4021&a=1522121579&isExternal=true&brandId=tvs.sbd.4000&id=518077009&l=en-GB&aec=UHD", imageUri = "https://is1-ssl.mzstatic.com/image/thumb/cZUkXfqYmSy57DBI5TiTMg/1000x563.webp", + languageTag = "en-CH", ), DemoItem.URL( title = "The Morning Show - Change: Season 2", uri = "https://play-edge.itunes.apple.com/WebObjects/MZPlayLocal.woa/hls/subscription/playlist.m3u8?cc=CH&svcId=tvs.vds.4021&a=1568297173&isExternal=true&brandId=tvs.sbd.4000&id=518034010&l=en-GB&aec=UHD", imageUri = "https://is1-ssl.mzstatic.com/image/thumb/IxmmS1rQ7ouO-pKoJsVpGw/1000x563.webp", + languageTag = "en-CH", ) - ) + ), + languageTag = "en-CH", ) private val thirdPartyStreams = Playlist( title = "Third-party streams", @@ -264,8 +302,10 @@ data class Playlist(val title: String, val items: List, val descriptio uri = "https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8", description = "4K video", imageUri = "https://i.ytimg.com/vi/d4_96ZWu3Vk/maxresdefault.jpg", + languageTag = "en-CH", ) - ) + ), + languageTag = "en-CH", ) private val bitmovinStreams = Playlist( title = "Bitmovin streams streams", @@ -274,28 +314,34 @@ data class Playlist(val title: String, val items: List, val descriptio title = "Multiple subtitles and audio tracks", uri = "https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8", imageUri = "https://durian.blender.org/wp-content/uploads/2010/06/05.8b_comp_000272.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "4K, HEVC", uri = "https://cdn.bitmovin.com/content/encoding_test_dash_hls/4k/hls/4k_profile/master.m3u8", imageUri = "https://peach.blender.org/wp-content/uploads/bbb-splash.png", + languageTag = "en-CH", ), DemoItem.URL( title = "VoD, single audio track", uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8", imageUri = "https://img.redbull.com/images/c_crop,w_3840,h_1920,x_0,y_0,f_auto,q_auto/c_scale,w_1200/redbullcom/tv/FO-1MR39KNMH2111/fo-1mr39knmh2111-featuremedia", + languageTag = "en-CH", ), DemoItem.URL( title = "AES-128", uri = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/m3u8s/11331.m3u8", imageUri = "https://img.redbull.com/images/c_crop,w_3840,h_1920,x_0,y_0,f_auto,q_auto/c_scale,w_1200/redbullcom/tv/FO-1MR39KNMH2111/fo-1mr39knmh2111-featuremedia", + languageTag = "en-CH", ), DemoItem.URL( title = "AVC Progressive", uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/MI201109210084_mpeg-4_hd_high_1080p25_10mbits.mp4", imageUri = "https://img.redbull.com/images/c_crop,w_3840,h_1920,x_0,y_0,f_auto,q_auto/c_scale,w_1200/redbullcom/tv/FO-1MR39KNMH2111/fo-1mr39knmh2111-featuremedia", + languageTag = "en-CH", ) - ) + ), + languageTag = "en-CH", ) private val unifiedStreaming = Playlist( title = "Unified Streaming - HLS", @@ -304,68 +350,82 @@ data class Playlist(val title: String, val items: List, val descriptio title = "Fragmented MP4", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Key Rotation", uri = "https://demo.unified-streaming.com/k8s/keyrotation/stable/keyrotation/keyrotation.isml/.m3u8", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "Alternate audio language", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Audio only", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.m3u8?filter=(type!=%22video%22)", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Trickplay", uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.m3u8", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Limiting bandwidth use", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?max_bitrate=800000", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Dynamic Track Selection", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8?filter=%28type%3D%3D%22audio%22%26%26systemBitrate%3C100000%29%7C%7C%28type%3D%3D%22video%22%26%26systemBitrate%3C1024000%29", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Pure live", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "Timeshift (5 minutes)", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?time_shift=300", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "Live audio", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?filter=(type!=%22video%22)", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "Pure live (scte35)", uri = "https://demo.unified-streaming.com/k8s/live/stable/scte35.isml/.m3u8", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "fMP4, clear", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-fmp4.ism/.m3u8", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "fMP4, HEVC 4K", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hevc.ism/.m3u8", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ) - ) + ), + languageTag = "en-CH", ) private val unifiedStreamingDash = Playlist( title = "Unified Streaming - Dash", @@ -374,78 +434,94 @@ data class Playlist(val title: String, val items: List, val descriptio title = "MP4", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Fragmented MP4", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Trickplay", uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Tiled thumbnails (live/timeline)", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-tiled-thumbnails-timeline.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Single - fragmented TTML", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-en.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Multiple - fragmented TTML", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-ttml.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Multiple - RFC 5646 language tags", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-rfc5646.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Accessibility - hard of hearing", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-hoh-subs.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Pure live", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "Timeshift (5 minutes)", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd?time_shift=300", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "DVB DASH low latency", uri = "https://demo.unified-streaming.com/k8s/live/stable/live-low-latency.isml/.mpd", imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", + languageTag = "en-CH", ), DemoItem.URL( title = "Audio only", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd?filter=(type!=%22video%22)", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Alternate audio language", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-lang.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Multiple audio codecs", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-multi-codec.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ), DemoItem.URL( title = "Accessibility - audio description", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-desc-aud.ism/.mpd", imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", + languageTag = "en-CH", ) - ) + ), + languageTag = "en-CH", ) private val aspectRatios = Playlist( title = "Aspect ratios", @@ -454,18 +530,22 @@ data class Playlist(val title: String, val items: List, val descriptio title = "Horizontal video", urn = "urn:rts:video:14827306", imageUri = "https://www.rts.ch/2024/04/10/19/23/14827621.image/16x9", + languageTag = "en-CH", ), DemoItem.URN( title = "Square video", urn = "urn:rts:video:8393241", imageUri = "https://www.rts.ch/2017/02/16/07/08/8393235.image/16x9", + languageTag = "en-CH", ), DemoItem.URN( title = "Vertical video", urn = "urn:rts:video:13444390", imageUri = "https://www.rts.ch/2022/10/06/17/32/13444380.image/4x5", + languageTag = "en-CH", ) - ) + ), + languageTag = "en-CH", ) private val unbufferedStreams = Playlist( title = "Unbuffered streams", @@ -475,14 +555,17 @@ data class Playlist(val title: String, val items: List, val descriptio uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0", description = "Live video (unbuffered)", imageUri = "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9", + languageTag = "fr-CH", ), DemoItem.URL( title = "Couleur 3 en direct", uri = "https://stream.srg-ssr.ch/m/couleur3/mp3_128", description = "Audio livestream (unbuffered)", imageUri = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=320&h=320", + languageTag = "fr-CH", ) - ) + ), + languageTag = "en-CH", ) private val cornerCases = Playlist( title = "Corner cases", @@ -492,21 +575,25 @@ data class Playlist(val title: String, val items: List, val descriptio urn = "urn:rts:video:13382911", description = "Content that is not available anymore", imageUri = "https://www.rts.ch/2022/09/20/09/57/13365589.image/16x9", + languageTag = "en-CH", ), DemoItem.URN( title = "Unknown URN", urn = "urn:srf:video:unknown", - description = "Content that does not exist" + description = "Content that does not exist", + languageTag = "en-CH", ), DemoItem.URL( title = "Custom MediaSource", uri = "https://custom-media.ch/fondue", - description = "Using a custom CustomMediaSource" + description = "Using a custom CustomMediaSource", + languageTag = "en-CH", ), BlockedTimeRangeAssetLoader.DemoItemBlockedTimeRangeAtStartAndEnd, BlockedTimeRangeAssetLoader.DemoItemBlockedTimeRangeOverlaps, BlockedTimeRangeAssetLoader.DemoItemBlockedTimeRangeIncluded, - ) + ), + languageTag = "en-CH", ) val examplesPlaylists = listOf( @@ -521,43 +608,52 @@ data class Playlist(val title: String, val items: List, val descriptio title = "Le R. - Légumes trop chers", uri = "https://rts-vod-amd.akamaized.net/ww/13444390/f1b478f7-2ae9-3166-94b9-c5d5fe9610df/master.m3u8", description = "Playlist item 1", + languageTag = "fr-CH", ), DemoItem.URL( title = "Le R. - Production de légumes bio", uri = "https://rts-vod-amd.akamaized.net/ww/13444333/feb1d08d-e62c-31ff-bac9-64c0a7081612/master.m3u8", description = "Playlist item 2", + languageTag = "fr-CH", ), DemoItem.URL( title = "Le R. - Endométriose", uri = "https://rts-vod-amd.akamaized.net/ww/13444466/2787e520-412f-35fb-83d7-8dbb31b5c684/master.m3u8", description = "Playlist item 3", + languageTag = "fr-CH", ), DemoItem.URL( title = "Le R. - Prix Nobel de littérature 2022", uri = "https://rts-vod-amd.akamaized.net/ww/13444447/c1d17174-ad2f-31c2-a084-846a9247fd35/master.m3u8", description = "Playlist item 4", + languageTag = "fr-CH", ), DemoItem.URL( title = "Le R. - Femme, vie, liberté", uri = "https://rts-vod-amd.akamaized.net/ww/13444352/32145dc0-b5f8-3a14-ae11-5fc6e33aaaa4/master.m3u8", description = "Playlist item 5", + languageTag = "fr-CH", ), DemoItem.URL( title = "Le R. - Attaque en Thaïlande", uri = "https://rts-vod-amd.akamaized.net/ww/13444409/23f808a4-b14a-3d3e-b2ed-fa1279f6cf01/master.m3u8", description = "Playlist item 6", + languageTag = "fr-CH", ), DemoItem.URL( title = "Le R. - Douches et vestiaires non genrés", uri = "https://rts-vod-amd.akamaized.net/ww/13444371/3f26467f-cd97-35f4-916f-ba3927445920/master.m3u8", description = "Playlist item 7", + languageTag = "fr-CH", ), DemoItem.URL( title = "Le R. - Prends soin de toi, des autres et à demain", uri = "https://rts-vod-amd.akamaized.net/ww/13444428/857d97ef-0b8e-306e-bf79-3b13e8c901e4/master.m3u8", description = "Playlist item 8", + languageTag = "fr-CH", ) - ) + ), + languageTag = "en-CH", ) val VideoUrns = Playlist( @@ -567,43 +663,52 @@ data class Playlist(val title: String, val items: List, val descriptio title = "Le R. - Légumes trop chers", urn = "urn:rts:video:13444390", description = "Playlist item 1", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le R. - Production de légumes bio", urn = "urn:rts:video:13444333", description = "Playlist item 2", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le R. - Endométriose", urn = "urn:rts:video:13444466", description = "Playlist item 3", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le R. - Prix Nobel de littérature 2022", urn = "urn:rts:video:13444447", description = "Playlist item 4", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le R. - Femme, vie, liberté", urn = "urn:rts:video:13444352", description = "Playlist item 5", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le R. - Attaque en Thailande", urn = "urn:rts:video:13444409", description = "Playlist item 6", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le R. - Douches et vestinaires non genrés", urn = "urn:rts:video:13444371", description = "Playlist item 7", + languageTag = "fr-CH", ), DemoItem.URN( title = "Le R. - Prend soin de toi des autres et à demain", urn = "urn:rts:video:13444428", description = "Playlist item 8", + languageTag = "fr-CH", ) - ) + ), + languageTag = "en-CH", ) val MixedContent = Playlist( @@ -613,7 +718,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.OnDemandHorizontalVideo, DemoItem.Unknown, DemoItem.ShortOnDemandVideoHLS - ) + ), + languageTag = "en-CH", ) val MixedContentLiveDvrVod = Playlist( @@ -623,7 +729,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.OnDemandHorizontalVideo, DemoItem.DvrVideo, DemoItem.ShortOnDemandVideoHLS - ) + ), + languageTag = "en-CH", ) val MixedContentLiveOnlyVod = Playlist( @@ -633,12 +740,14 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.OnDemandHorizontalVideo, DemoItem.LiveVideo, DemoItem.ShortOnDemandVideoHLS, - ) + ), + languageTag = "en-CH", ) val EmptyPlaylist = Playlist( title = "Empty", items = emptyList(), + languageTag = "en-CH", ) val StreamUrls = Playlist( @@ -654,7 +763,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.OnDemandAudioMP3, DemoItem.LiveAudioMP3, DemoItem.DvrAudioHLS - ) + ), + languageTag = "en-CH", ) val StreamUrns = Playlist( @@ -670,7 +780,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.OnDemandAudio, DemoItem.Expired, DemoItem.Unknown, - ) + ), + languageTag = "en-CH", ) val StreamApples = Playlist( @@ -684,7 +795,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.AppleAtmos, DemoItem.AppleWWDC_2023, DemoItem.AppleTvSample, - ) + ), + languageTag = "en-CH", ) val StreamGoogles = Playlist( @@ -694,7 +806,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.GoogleDashH264_CENC_Widewine, DemoItem.GoogleDashH265, DemoItem.GoogleDashH265_CENC_Widewine - ) + ), + languageTag = "en-CH", ) val BitmovinSamples = Playlist( @@ -705,7 +818,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.BitmovinOnDemand_4K_HEVC, DemoItem.BitmovinOnDemandAES128, DemoItem.BitmovinOnDemandSingleAudio, - ) + ), + languageTag = "en-CH", ) val UnifiedStreamingHls = Playlist( @@ -723,7 +837,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.UnifiedStreamingOnDemandTrickplay, DemoItem.UnifiedStreamingOnDemandLimitedBandwidth, DemoItem.UnifiedStreamingOnDemandDynamicTrackSelection, - ) + ), + languageTag = "en-CH", ) val UnifiedStreamingDash = Playlist( @@ -744,7 +859,8 @@ data class Playlist(val title: String, val items: List, val descriptio DemoItem.UnifiedStreamingOnDemand_Dash_AlternateAudioLanguage, DemoItem.UnifiedStreamingOnDemand_Dash_Multiple_Audio_Codec, DemoItem.UnifiedStreamingOnDemand_Dash_AccessibilityAudio, - ) + ), + languageTag = "en-CH", ) val All = Playlist( @@ -756,7 +872,8 @@ data class Playlist(val title: String, val items: List, val descriptio StreamApples.items + UnifiedStreamingHls.items + UnifiedStreamingDash.items + - BitmovinSamples.items + BitmovinSamples.items, + languageTag = "en-CH", ) } } diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/source/BlockedTimeRangeAssetLoader.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/source/BlockedTimeRangeAssetLoader.kt index c031e6570..0a1c9786c 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/source/BlockedTimeRangeAssetLoader.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/source/BlockedTimeRangeAssetLoader.kt @@ -78,7 +78,7 @@ class BlockedTimeRangeAssetLoader(context: Context) : AssetLoader(DefaultMediaSo } } - @Suppress("UndocumentedPublicClass") + @Suppress("StringLiteralDuplication", "UndocumentedPublicClass") companion object { private val URL = DemoItem.AppleBasic_16_9_TS_HLS.uri private val videoDuration = 1800.05.seconds @@ -94,6 +94,7 @@ class BlockedTimeRangeAssetLoader(context: Context) : AssetLoader(DefaultMediaSo title = "Starts and ends with a blocked time range", uri = ID_START_END, description = "Blocked times ranges at 00:00 - 00:10 and 25:00 - 30:00", + languageTag = "en-CH", ) /** @@ -102,7 +103,8 @@ class BlockedTimeRangeAssetLoader(context: Context) : AssetLoader(DefaultMediaSo val DemoItemBlockedTimeRangeOverlaps = DemoItem.URL( title = "Blocked time ranges are overlapping", uri = ID_OVERLAP, - description = "Blocked times ranges at 00:10 to 00:50 and 00:15 to 05:00" + description = "Blocked times ranges at 00:10 to 00:50 and 00:15 to 05:00", + languageTag = "en-CH", ) /** @@ -111,7 +113,8 @@ class BlockedTimeRangeAssetLoader(context: Context) : AssetLoader(DefaultMediaSo val DemoItemBlockedTimeRangeIncluded = DemoItem.URL( title = "Blocked time range is included in an other one", uri = ID_INCLUDED, - description = "Blocked times ranges at 00:15 - 00:30 and 00:10 - 01:00" + description = "Blocked times ranges at 00:15 - 00:30 and 00:10 - 01:00", + languageTag = "en-CH", ) } } diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/examples/ExamplesViewModel.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/examples/ExamplesViewModel.kt index 6b0001f5c..7ae9c9c5b 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/examples/ExamplesViewModel.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/examples/ExamplesViewModel.kt @@ -42,6 +42,7 @@ class ExamplesViewModel(application: Application) : AndroidViewModel(application urn = item.urn, description = "DRM-protected video", imageUri = item.imageUrl.rawUrl, + languageTag = "fr-CH", ) } val listTokenProtectedContent = repository.getTvLiveCenter(Bu.RTS, PROTECTED_CONTENT_PAGE_SIZE).getOrDefault(emptyList()) @@ -51,6 +52,7 @@ class ExamplesViewModel(application: Application) : AndroidViewModel(application urn = item.urn, description = "Token-protected video", imageUri = item.imageUrl.rawUrl, + languageTag = "fr-CH", ) } val allProtectedContent = listDrmContent + listTokenProtectedContent @@ -60,7 +62,8 @@ class ExamplesViewModel(application: Application) : AndroidViewModel(application } else { val protectedPlaylist = Playlist( title = "Protected streams (URNs)", - items = allProtectedContent + items = allProtectedContent, + languageTag = "en-CH", ) val updatedPlaylists = Playlist.examplesPlaylists.toMutableList() .apply { diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt index 22696fa14..131b2219c 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/ContentList.kt @@ -5,6 +5,11 @@ package ch.srgssr.pillarbox.demo.shared.ui.integrationLayer import ch.srg.dataProvider.integrationlayer.request.parameters.Bu +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.RSI +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.RTR +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.RTS +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.SRF +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.SWI import kotlinx.serialization.Serializable /** @@ -14,6 +19,7 @@ import kotlinx.serialization.Serializable @Suppress("UndocumentedPublicProperty", "UndocumentedPublicClass") sealed interface ContentList { val destinationTitle: String + val languageTag: String? // Type-safe navigation does not yet support property-level @Serializable // So, we use the BU name as a property, and recreate the BU on demand @@ -24,6 +30,16 @@ sealed interface ContentList { val bu: Bu get() = Bu(buName) + override val languageTag: String? + get() = when (bu) { + SRF -> "de-CH" + RTS -> "fr-CH" + RTR -> "rm-CH" + SWI -> "de-CH" + RSI -> "it-CH" + else -> null + } + override val destinationTitle: String get() = bu.name.uppercase() } @@ -39,7 +55,8 @@ sealed interface ContentList { @Serializable data class LatestMediaForTopic( val urn: String, - val topic: String + val topic: String, + override val languageTag: String?, ) : ContentList { override val destinationTitle = topic } @@ -47,7 +64,8 @@ sealed interface ContentList { @Serializable data class LatestMediaForShow( val urn: String, - val show: String + val show: String, + override val languageTag: String?, ) : ContentList { override val destinationTitle = show } diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSection.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSection.kt index 5942ea7ef..cec79071f 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSection.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSection.kt @@ -11,8 +11,10 @@ import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.ContentList * * @property title The title of the section. * @property contentList The list of elements in the section. + * @property languageTag The IETF BCP47 language tag of the title. */ data class ContentListSection( val title: String, - val contentList: List + val contentList: List, + val languageTag: String?, ) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt index b56393164..d809a98ce 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/integrationLayer/data/ContentListSections.kt @@ -16,14 +16,15 @@ private val busWithoutSWI = listOf(Bu.RSI, Bu.RTR, Bu.RTS, Bu.SRF) /** * All the sections available in the "Lists" tab. */ +@Suppress("StringLiteralDuplication") val contentListSections = listOf( - ContentListSection("TV Topics", bus.map { ContentList.TVTopics(it) }), - ContentListSection("TV Shows", bus.map { ContentList.TVShows(it) }), - ContentListSection("TV Latest Videos", bus.map { ContentList.TVLatestMedias(it) }), - ContentListSection("TV Livestreams", busWithoutSWI.map { ContentList.TVLivestreams(it) }), - ContentListSection("TV Live Center", busWithoutSWI.map { ContentList.TVLiveCenter(it) }), - ContentListSection("TV Live Web", busWithoutSWI.map { ContentList.TVLiveWeb(it) }), - ContentListSection("Radio Livestreams", busWithoutSWI.map { ContentList.RadioLiveStreams(it) }), - ContentListSection("Radio Latest Audios", busWithoutSWI.map { ContentList.RadioLatestMedias(it) }), - ContentListSection("Radio Shows", busWithoutSWI.map { ContentList.RadioShows(it) }), + ContentListSection("TV Topics", bus.map { ContentList.TVTopics(it) }, languageTag = "en-CH"), + ContentListSection("TV Shows", bus.map { ContentList.TVShows(it) }, languageTag = "en-CH"), + ContentListSection("TV Latest Videos", bus.map { ContentList.TVLatestMedias(it) }, languageTag = "en-CH"), + ContentListSection("TV Livestreams", busWithoutSWI.map { ContentList.TVLivestreams(it) }, languageTag = "en-CH"), + ContentListSection("TV Live Center", busWithoutSWI.map { ContentList.TVLiveCenter(it) }, languageTag = "en-CH"), + ContentListSection("TV Live Web", busWithoutSWI.map { ContentList.TVLiveWeb(it) }, languageTag = "en-CH"), + ContentListSection("Radio Livestreams", busWithoutSWI.map { ContentList.RadioLiveStreams(it) }, languageTag = "en-CH"), + ContentListSection("Radio Latest Audios", busWithoutSWI.map { ContentList.RadioLatestMedias(it) }, languageTag = "en-CH"), + ContentListSection("Radio Shows", busWithoutSWI.map { ContentList.RadioShows(it) }, languageTag = "en-CH"), ) diff --git a/pillarbox-demo-shared/src/main/res/values/strings.xml b/pillarbox-demo-shared/src/main/res/values/strings.xml index 0b521a2d2..7f5ab6d58 100644 --- a/pillarbox-demo-shared/src/main/res/values/strings.xml +++ b/pillarbox-demo-shared/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Examples Lists Search + Change BU Showcases Search for content No results diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt index 4a7b62627..0fcf41e9b 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt @@ -31,6 +31,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -191,6 +196,9 @@ private fun ListsMenu( currentLocation: IlLocation?, onServerSelected: (server: URL, forceSAM: Boolean, location: IlLocation?) -> Unit ) { + val context = LocalContext.current + val servers = remember { getServers(context).groupBy { it.serverName }.values } + var isMenuVisible by remember { mutableStateOf(false) } IconButton(onClick = { isMenuVisible = true }) { @@ -203,17 +211,23 @@ private fun ListsMenu( DropdownMenu( expanded = isMenuVisible, onDismissRequest = { isMenuVisible = false }, + modifier = Modifier.semantics { + collectionInfo = CollectionInfo( + rowCount = servers.fold(0) { value, servers -> + value + servers.size + }, + columnCount = 1, + ) + }, offset = DpOffset( x = MaterialTheme.paddings.small, y = 0.dp, ), ) { - val context = LocalContext.current val currentServerUrl = currentServer.toString() - val servers = remember { getServers(context).groupBy { it.serverName }.values } servers.forEachIndexed { index, environmentConfig -> - environmentConfig.forEach { config -> + environmentConfig.forEachIndexed { envIndex, config -> val isSelected = currentServerUrl == config.host.toString() && currentForceSAM == config.forceSAM && currentLocation == config.location @@ -224,6 +238,14 @@ private fun ListsMenu( onServerSelected(config.host, config.forceSAM, config.location) isMenuVisible = false }, + modifier = Modifier.semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = (index * servers.size) + envIndex, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, trailingIcon = if (isSelected) { { Icon( diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt index e11350342..782044350 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt @@ -29,30 +29,35 @@ import kotlin.time.Duration.Companion.seconds * * @param content The content to display. * @param modifier The [Modifier] to apply to the component. + * @param languageTag The IETF BCP47 language tag of the content. * @param onClick The action to perform when clicking the component. */ @Composable fun ContentView( content: Content, modifier: Modifier = Modifier, + languageTag: String? = null, onClick: () -> Unit ) { when (content) { is Content.Topic -> DemoListItemView( title = content.title, modifier = modifier.fillMaxWidth(), + languageTag = languageTag, onClick = onClick ) is Content.Show -> DemoListItemView( title = content.title, modifier = modifier.fillMaxWidth(), + languageTag = languageTag, onClick = onClick ) is Content.Media -> MediaView( content = content, modifier = modifier.fillMaxWidth(), + languageTag = languageTag, onClick = onClick ) @@ -60,6 +65,7 @@ fun ContentView( title = content.title, modifier = modifier.fillMaxWidth(), subtitle = content.description, + languageTag = languageTag, onClick = onClick ) } @@ -69,6 +75,7 @@ fun ContentView( private fun MediaView( content: Content.Media, modifier: Modifier = Modifier, + languageTag: String? = null, onClick: () -> Unit ) { val mediaTypeIcon = when (content.mediaType) { @@ -86,6 +93,7 @@ private fun MediaView( title = content.title, modifier = modifier, subtitle = "$mediaTypeIcon $subtitlePrefix ${content.date} - $duration", + languageTag = languageTag, onClick = onClick ) } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListHeaderView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListHeaderView.kt index 6f7e57d0c..842ac6f39 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListHeaderView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListHeaderView.kt @@ -9,6 +9,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.intl.LocaleList +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.ui.theme.paddings @@ -18,14 +23,22 @@ import ch.srgssr.pillarbox.demo.ui.theme.paddings * * @param title The title of the header. * @param modifier The [Modifier] of the layout. + * @param languageTag The IETF BCP47 language tag of the title. */ @Composable fun DemoListHeaderView( title: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + languageTag: String? = null, ) { + val localeList = languageTag?.let { LocaleList(Locale(languageTag)) } + Text( - text = title, + text = buildAnnotatedString { + withStyle(SpanStyle(localeList = localeList)) { + append(title) + } + }, modifier = modifier.padding( top = MaterialTheme.paddings.baseline, bottom = MaterialTheme.paddings.small diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListItemView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListItemView.kt index f4b2c3735..422fcfdf1 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListItemView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/DemoListItemView.kt @@ -20,8 +20,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.intl.LocaleList import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.ui.theme.paddings @@ -32,6 +37,7 @@ import ch.srgssr.pillarbox.demo.ui.theme.paddings * @param title The title of the item. * @param modifier The [Modifier] to apply to the root of the item. * @param subtitle The optional subtitle of the item. + * @param languageTag The IETF BCP47 language tag of the title and subtitle. * @param onClick The action to perform when an item is clicked. */ @Composable @@ -39,6 +45,7 @@ fun DemoListItemView( title: String, modifier: Modifier = Modifier, subtitle: String? = null, + languageTag: String? = null, onClick: () -> Unit, ) { Column( @@ -50,8 +57,14 @@ fun DemoListItemView( vertical = MaterialTheme.paddings.small ) ) { + val localeList = languageTag?.let { LocaleList(Locale(languageTag)) } + Text( - text = title, + text = buildAnnotatedString { + withStyle(SpanStyle(localeList = localeList)) { + append(title) + } + }, overflow = TextOverflow.Ellipsis, maxLines = 1, style = MaterialTheme.typography.bodyMedium @@ -59,7 +72,11 @@ fun DemoListItemView( if (!subtitle.isNullOrBlank()) { Text( - text = subtitle, + text = buildAnnotatedString { + withStyle(SpanStyle(localeList = localeList)) { + append(subtitle) + } + }, modifier = Modifier.padding(top = MaterialTheme.paddings.micro), color = MaterialTheme.colorScheme.outline, overflow = TextOverflow.Ellipsis, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt index b725c4d95..63d1f88c1 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/ExamplesHome.kt @@ -72,7 +72,8 @@ private fun ListStreamView( ) { playlist -> DemoListHeaderView( title = playlist.title, - modifier = Modifier.padding(start = MaterialTheme.paddings.baseline) + modifier = Modifier.padding(start = MaterialTheme.paddings.baseline), + languageTag = playlist.languageTag, ) DemoListSectionView { @@ -81,6 +82,7 @@ private fun ListStreamView( title = item.title ?: "No title", modifier = Modifier.fillMaxWidth(), subtitle = item.description, + languageTag = item.languageTag, onClick = { onItemClicked(item) }, ) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt index 308f78258..7ffd42416 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsHome.kt @@ -14,6 +14,11 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController @@ -55,7 +60,8 @@ fun NavGraphBuilder.listsNavGraph( is Content.Show -> { val nextContentList = ContentList.LatestMediaForShow( urn = content.urn, - show = content.title + show = content.title, + languageTag = contentList.languageTag, ) navController.navigate(nextContentList) @@ -64,7 +70,8 @@ fun NavGraphBuilder.listsNavGraph( is Content.Topic -> { val nextContentList = ContentList.LatestMediaForTopic( urn = content.urn, - topic = content.title + topic = content.title, + languageTag = contentList.languageTag, ) navController.navigate(nextContentList) @@ -77,6 +84,7 @@ fun NavGraphBuilder.listsNavGraph( host = ilHost, forceSAM = forceSAM, ilLocation = ilLocation, + languageTag = contentList.languageTag, ) SimplePlayerActivity.startActivity(navController.context, item) @@ -155,6 +163,7 @@ private inline fun NavGraphBuilder.addContentListRoute title = contentList.destinationTitle, items = viewModel.data.collectAsLazyPagingItems(), modifier = Modifier.fillMaxWidth(), + languageTag = contentList.languageTag, contentClick = { onClick(contentList, it) } ) } @@ -173,14 +182,29 @@ private fun ListsHome(onContentSelected: (ContentList) -> Unit) { items(contentListSections) { section -> DemoListHeaderView( title = section.title, - modifier = Modifier.padding(start = MaterialTheme.paddings.baseline) + modifier = Modifier.padding(start = MaterialTheme.paddings.baseline), + languageTag = section.languageTag, ) - DemoListSectionView { + DemoListSectionView( + modifier = Modifier.semantics { + collectionInfo = CollectionInfo(rowCount = section.contentList.size, columnCount = 1) + }, + ) { section.contentList.forEachIndexed { index, item -> DemoListItemView( title = item.destinationTitle, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, + languageTag = item.languageTag, onClick = { onContentSelected(item) } ) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt index ab8bb2d91..74e57ce90 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/lists/ListsSubSection.kt @@ -20,6 +20,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.paging.LoadState import androidx.paging.LoadStates @@ -50,6 +55,7 @@ import kotlin.time.Duration.Companion.seconds * @param title The title of the list. * @param items The list of items to display. * @param modifier The [Modifier] to apply to the root of the list. + * @param languageTag The IETF BCP47 language tag of the title. * @param contentClick The action to perform when clicking on an item. */ @Composable @@ -57,6 +63,7 @@ fun ListsSubSection( title: String, items: LazyPagingItems, modifier: Modifier = Modifier, + languageTag: String? = null, contentClick: (Content) -> Unit ) { when (val loadState = items.loadState.refresh) { @@ -66,7 +73,9 @@ fun ListsSubSection( EmptyView(modifier = modifier) } else { LazyColumn( - modifier = modifier, + modifier = modifier.semantics { + collectionInfo = CollectionInfo(rowCount = items.itemCount, columnCount = 1) + }, contentPadding = PaddingValues( start = MaterialTheme.paddings.baseline, end = MaterialTheme.paddings.baseline, @@ -76,7 +85,8 @@ fun ListsSubSection( item(contentType = "title") { DemoListHeaderView( title = title, - modifier = Modifier.padding(start = MaterialTheme.paddings.baseline) + modifier = Modifier.padding(start = MaterialTheme.paddings.baseline), + languageTag = languageTag, ) } @@ -109,7 +119,16 @@ fun ListsSubSection( color = MaterialTheme.colorScheme.surfaceVariant, shape = shape ) - .clip(shape), + .clip(shape) + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, + languageTag = languageTag, onClick = { contentClick(item) } ) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt index 316fce641..63ea27c1a 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt @@ -219,7 +219,7 @@ class SimplePlayerActivity : ComponentActivity(), ServiceConnection { * Start activity [SimplePlayerActivity] with DemoItem. */ fun startActivity(context: Context, item: DemoItem) { - startActivity(context, Playlist("UniqueItem", listOf(item))) + startActivity(context, Playlist("UniqueItem", listOf(item), "en-CH")) } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt index c8f26bd6f..412a6d4e2 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/MediaItemLibrary.kt @@ -29,7 +29,9 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import ch.srgssr.pillarbox.demo.R import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme @@ -69,7 +71,7 @@ fun MediaItemLibraryDialog( ) { Column { Text( - text = "Add to the playlist", + text = stringResource(R.string.add_to_playlist), modifier = Modifier.padding(MaterialTheme.paddings.baseline), color = AlertDialogDefaults.titleContentColor, style = MaterialTheme.typography.headlineSmall, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistItemView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistItemView.kt index 7eea522ff..458e58e25 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistItemView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistItemView.kt @@ -19,9 +19,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import ch.srgssr.pillarbox.demo.R import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme /** @@ -73,7 +75,7 @@ fun PlaylistItemView( ) { Icon( imageVector = Icons.Default.ArrowDownward, - contentDescription = "Move bottom of the list" + contentDescription = stringResource(R.string.move_down), ) } IconButton( @@ -83,7 +85,7 @@ fun PlaylistItemView( ) { Icon( imageVector = Icons.Default.ArrowUpward, - contentDescription = "Move top of the list" + contentDescription = stringResource(R.string.move_up), ) } IconButton( @@ -93,7 +95,7 @@ fun PlaylistItemView( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Delete" + contentDescription = stringResource(R.string.remove), ) } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistView.kt index 73354de12..32db9c4c2 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/playlist/PlaylistView.kt @@ -32,11 +32,18 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player +import ch.srgssr.pillarbox.demo.R import ch.srgssr.pillarbox.demo.shared.data.Playlist import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.ui.theme.paddings @@ -113,7 +120,11 @@ private fun PlaylistView( onShuffleToggled: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier) { + LazyColumn( + modifier = modifier.semantics { + collectionInfo = CollectionInfo(rowCount = mediaItems.size, columnCount = 1) + }, + ) { stickyHeader { Row( modifier = Modifier @@ -125,17 +136,24 @@ private fun PlaylistView( IconButton( onClick = onAddToPlaylistClick ) { - Icon(imageVector = Icons.Default.Add, contentDescription = null) + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(R.string.add_to_playlist), + ) } IconToggleButton(checked = shuffleEnabled, onShuffleToggled) { - if (shuffleEnabled) { - Icon(imageVector = Icons.Default.ShuffleOn, contentDescription = null) - } else { - Icon(imageVector = Icons.Default.Shuffle, contentDescription = null) - } + val imageVector = if (shuffleEnabled) Icons.Default.ShuffleOn else Icons.Default.Shuffle + + Icon( + imageVector = imageVector, + contentDescription = stringResource(R.string.toggle_shuffle), + ) } IconButton(onClick = onRemoveAll) { - Icon(imageVector = Icons.Default.DeleteForever, contentDescription = null) + Icon( + imageVector = Icons.Default.DeleteForever, + contentDescription = stringResource(R.string.clear_playlist), + ) } } } @@ -148,6 +166,14 @@ private fun PlaylistView( val canMoveDown = nextIndex < mediaItems.size PlaylistItemView( modifier = Modifier + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + } .clickable(enabled = index != currentMediaItemIndex) { onItemClick(mediaItem, index) }, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt index 793f468f4..e86348032 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt @@ -55,7 +55,20 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.intl.LocaleList +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -64,6 +77,11 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey import ch.srg.dataProvider.integrationlayer.request.parameters.Bu +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.RSI +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.RTR +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.RTS +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.SRF +import ch.srg.dataProvider.integrationlayer.request.parameters.Bu.Companion.SWI import ch.srgssr.pillarbox.demo.R import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.SearchViewModel import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.Content @@ -86,6 +104,7 @@ fun SearchHome( onSearchClicked: (media: Content.Media) -> Unit ) { val lazyItems = searchViewModel.result.collectAsLazyPagingItems() + val focusRequester = remember { FocusRequester() } val currentBu by searchViewModel.bu.collectAsState() val searchQuery by searchViewModel.query.collectAsState() @@ -99,6 +118,7 @@ fun SearchHome( query = searchQuery, bus = bus, selectedBu = currentBu, + focusRequester = focusRequester, modifier = Modifier.fillMaxWidth(), onBuChange = searchViewModel::selectBu, onClearClick = searchViewModel::clear, @@ -108,6 +128,8 @@ fun SearchHome( SearchResultList( searchViewModel = searchViewModel, items = lazyItems, + focusRequester = focusRequester, + currentBu = currentBu, contentClick = onSearchClicked ) } @@ -117,6 +139,8 @@ fun SearchHome( private fun SearchResultList( searchViewModel: SearchViewModel, items: LazyPagingItems, + focusRequester: FocusRequester, + currentBu: Bu, contentClick: (Content.Media) -> Unit, modifier: Modifier = Modifier ) { @@ -128,7 +152,19 @@ private fun SearchResultList( if (searchViewModel.hasValidSearchQuery()) { NoResult(modifier = modifier.fillMaxSize()) } else { - NoContent(modifier = modifier.fillMaxSize()) + val softwareKeyboardController = LocalSoftwareKeyboardController.current + + NoContent( + modifier = modifier + .fillMaxSize() + .semantics { + onClick { + focusRequester.requestFocus() + softwareKeyboardController?.show() + true + } + }, + ) } } else { LazyColumn(modifier = modifier) { @@ -161,6 +197,7 @@ private fun SearchResultList( shape = shape ) .clip(shape), + languageTag = currentBu.languageTag, onClick = { contentClick(item) } ) @@ -196,22 +233,31 @@ private fun SearchInput( query: String, bus: List, selectedBu: Bu, + focusRequester: FocusRequester, modifier: Modifier = Modifier, onBuChange: (bu: Bu) -> Unit, onClearClick: () -> Unit, onQueryChange: (query: String) -> Unit ) { - val focusRequester = remember { FocusRequester() } - SearchBar( inputField = { + val softwareKeyboardController = LocalSoftwareKeyboardController.current + SearchBarDefaults.InputField( query = query, onQueryChange = onQueryChange, onSearch = {}, expanded = false, onExpandedChange = {}, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .semantics { + onClick { + focusRequester.requestFocus() + softwareKeyboardController?.show() + true + } + }, placeholder = { Text(text = stringResource(sharedR.string.search_placeholder)) }, leadingIcon = { var showBuSelector by remember { mutableStateOf(false) } @@ -222,6 +268,7 @@ private fun SearchInput( .clickable( interactionSource = null, indication = null, + onClickLabel = stringResource(sharedR.string.change_bu), ) { showBuSelector = true } @@ -236,7 +283,7 @@ private fun SearchInput( label = "icon_rotation_animation" ) - Text(text = selectedBu.name.uppercase()) + BuLabel(selectedBu) Icon( imageVector = Icons.Default.ExpandMore, @@ -248,18 +295,29 @@ private fun SearchInput( DropdownMenu( expanded = showBuSelector, onDismissRequest = { showBuSelector = false }, + modifier = Modifier.semantics { + collectionInfo = CollectionInfo(rowCount = bus.size, columnCount = 1) + }, offset = DpOffset( x = 0.dp, y = MaterialTheme.paddings.small ) ) { - bus.forEach { bu -> + bus.forEachIndexed { index, bu -> DropdownMenuItem( - text = { Text(text = bu.name.uppercase()) }, + text = { BuLabel(bu) }, onClick = { onBuChange(bu) showBuSelector = false }, + modifier = Modifier.semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, trailingIcon = if (selectedBu == bu) { { Icon( @@ -302,6 +360,35 @@ private fun SearchInput( } } +private val Bu.languageTag: String? + get() { + return when (this) { + SRF -> "de-CH" + RTS -> "fr-CH" + RTR -> "rm-CH" + SWI -> "de-CH" + RSI -> "it-CH" + else -> null + } + } + +@Composable +private fun BuLabel( + bu: Bu, + modifier: Modifier = Modifier, +) { + val localeList = bu.languageTag?.let { LocaleList(Locale(it)) } + + Text( + text = buildAnnotatedString { + withStyle(SpanStyle(localeList = localeList)) { + append(bu.name.uppercase()) + } + }, + modifier = modifier, + ) +} + @Composable private fun NoContent(modifier: Modifier = Modifier) { StateMessage(modifier = modifier, message = stringResource(sharedR.string.empty_search_query), image = Icons.Default.Search) @@ -315,7 +402,7 @@ private fun NoResult(modifier: Modifier = Modifier) { @Composable private fun StateMessage(modifier: Modifier, message: String, image: ImageVector) { Column( - modifier = modifier, + modifier = modifier.semantics(mergeDescendants = true) {}, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -351,7 +438,8 @@ private fun ErrorView(error: Throwable, modifier: Modifier = Modifier) { Text( text = error.localizedMessage ?: error.message ?: "Error", style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.error + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, ) } } @@ -363,7 +451,8 @@ private fun SearchInputPreview() { SearchInput( query = "Query", bus = bus, - selectedBu = Bu.RTS, + selectedBu = RTS, + focusRequester = remember { FocusRequester() }, onBuChange = {}, onClearClick = {}, onQueryChange = {} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt index 5affda339..81801f031 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt @@ -7,7 +7,6 @@ package ch.srgssr.pillarbox.demo.ui.settings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource @@ -20,6 +19,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -43,6 +43,14 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -96,7 +104,10 @@ private fun MetricsOverlaySettings( setMetricsOverlayTextSize: (AppSettings.TextSize) -> Unit, ) { SettingSection(title = stringResource(R.string.setting_metrics_overlay)) { - TextLabel(text = stringResource(R.string.settings_enabled_overlay_description)) + TextLabel( + text = stringResource(R.string.settings_enabled_overlay_description), + modifier = Modifier.padding(top = MaterialTheme.paddings.small), + ) LabeledSwitch( text = stringResource(R.string.settings_enabled_metrics_overlay), @@ -135,7 +146,9 @@ private fun LibraryVersionSection() { DemoListItemView( leadingText = "Pillarbox", trailingText = BuildConfig.VERSION_NAME, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .minimumInteractiveComponentSize(), ) HorizontalDivider() @@ -143,7 +156,9 @@ private fun LibraryVersionSection() { DemoListItemView( leadingText = "Media3", trailingText = MediaLibraryInfo.VERSION, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .minimumInteractiveComponentSize(), ) } } @@ -185,7 +200,7 @@ private fun LabeledSwitch( ) { Row( modifier = modifier - .clickable { onCheckedChange(!checked) } + .toggleable(checked, onValueChange = onCheckedChange) .minimumInteractiveComponentSize() .padding(end = MaterialTheme.paddings.baseline), horizontalArrangement = Arrangement.SpaceBetween, @@ -216,6 +231,13 @@ private fun DropdownSetting( Box(modifier = modifier) { Row( modifier = Modifier + .semantics(mergeDescendants = true) { + role = Role.DropdownList + onClick(text) { + showDropdownMenu = true + true + } + } .fillMaxWidth() .pointerInput(Unit) { detectTapGestures( @@ -270,15 +292,26 @@ private fun DropdownSetting( DropdownMenu( expanded = showDropdownMenu, onDismissRequest = { showDropdownMenu = false }, + modifier = Modifier.semantics { + collectionInfo = CollectionInfo(rowCount = entries.size, columnCount = 1) + }, offset = dropdownOffset, ) { - entries.forEach { entry -> + entries.forEachIndexed { index, entry -> DropdownMenuItem( text = { Text(text = entry.toString()) }, onClick = { onEntrySelected(entry) showDropdownMenu = false }, + modifier = Modifier.semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, leadingIcon = { AnimatedVisibility(entry == selectedEntry) { Icon( diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt index 1e49f7cec..dc8744d8a 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt @@ -17,6 +17,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.navigation.NavController import ch.srgssr.pillarbox.demo.R import ch.srgssr.pillarbox.demo.shared.data.Playlist @@ -50,7 +55,23 @@ fun ShowcasesHome(navController: NavController) { start = MaterialTheme.paddings.baseline, top = MaterialTheme.paddings.small ) - val itemModifier = Modifier.fillMaxWidth() + val sectionModifier = { size: Int -> + Modifier.semantics { + collectionInfo = CollectionInfo(rowCount = size, columnCount = 1) + } + } + val itemModifier = { index: Int -> + Modifier + .fillMaxWidth() + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + } + } Column( modifier = Modifier @@ -58,41 +79,43 @@ fun ShowcasesHome(navController: NavController) { .padding(horizontal = MaterialTheme.paddings.baseline) .padding(bottom = MaterialTheme.paddings.baseline) ) { + val layoutDestinations = listOf( + stringResource(R.string.simple_player) to NavigationRoutes.SimplePlayer, + stringResource(R.string.story) to NavigationRoutes.Story, + stringResource(R.string.chapters) to NavigationRoutes.Chapters, + stringResource(R.string.thumbnail) to NavigationRoutes.ThumbnailShowcase, + ) + val miscDestinations = listOf( + stringResource(R.string.start_given_time_example) to NavigationRoutes.StartAtGivenTime, + stringResource(R.string.showcase_time_based_content) to NavigationRoutes.TimeBasedContent, + stringResource(R.string.adaptive) to NavigationRoutes.Adaptive, + stringResource(R.string.player_swap) to NavigationRoutes.PlayerSwap, + stringResource(R.string.tracker_example) to NavigationRoutes.TrackingSample, + stringResource(R.string.update_media_item_example) to NavigationRoutes.UpdatableSample, + stringResource(R.string.smooth_seeking_example) to NavigationRoutes.SmoothSeeking, + stringResource(R.string.video_360) to NavigationRoutes.Video360, + stringResource(R.string.showcase_countdown) to NavigationRoutes.CountdownShowcase, + ) + DemoListHeaderView( title = stringResource(R.string.layouts), modifier = Modifier.padding(start = MaterialTheme.paddings.baseline) ) - DemoListSectionView { - DemoListItemView( - title = stringResource(R.string.simple_player), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.SimplePlayer) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.story), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.Story) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.chapters), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.Chapters) } - ) - - HorizontalDivider() + DemoListSectionView( + modifier = sectionModifier(layoutDestinations.size), + ) { + layoutDestinations.forEachIndexed { index, (label, destination) -> + DemoListItemView( + title = label, + modifier = itemModifier(index), + onClick = { navController.navigate(destination) } + ) - DemoListItemView( - title = stringResource(R.string.thumbnail), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.ThumbnailShowcase) } - ) + if (index < layoutDestinations.lastIndex) { + HorizontalDivider() + } + } } DemoListHeaderView( @@ -100,12 +123,14 @@ fun ShowcasesHome(navController: NavController) { modifier = titleModifier ) - DemoListSectionView { - playlists.forEach { item -> + DemoListSectionView( + modifier = sectionModifier(playlists.size + 1), + ) { + playlists.forEachIndexed { index, item -> DemoListItemView( title = item.title, - modifier = itemModifier, - subtitle = item.description, + modifier = itemModifier(index), + languageTag = item.languageTag, onClick = { SimplePlayerActivity.startActivity(context, item) } ) @@ -114,7 +139,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.showcase_playback_settings), - modifier = itemModifier, + modifier = itemModifier(playlists.size), onClick = { navController.navigate(NavigationRoutes.ShowcasePlaybackSettings) }, ) } @@ -124,10 +149,12 @@ fun ShowcasesHome(navController: NavController) { modifier = titleModifier ) - DemoListSectionView { + DemoListSectionView( + modifier = sectionModifier(2), + ) { DemoListItemView( title = stringResource(R.string.exoplayer_view), - modifier = itemModifier, + modifier = itemModifier(0), onClick = { navController.navigate(NavigationRoutes.ExoPlayerSample) } ) @@ -135,7 +162,7 @@ fun ShowcasesHome(navController: NavController) { DemoListItemView( title = stringResource(R.string.auto), - modifier = itemModifier, + modifier = itemModifier(1), onClick = { val intent = Intent(context, MediaControllerActivity::class.java) context.startActivity(intent) @@ -148,75 +175,20 @@ fun ShowcasesHome(navController: NavController) { modifier = titleModifier ) - DemoListSectionView { - DemoListItemView( - title = stringResource(R.string.start_given_time_example), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.StartAtGivenTime) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.showcase_time_based_content), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.TimeBasedContent) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.adaptive), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.Adaptive) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.player_swap), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.PlayerSwap) } - ) - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.tracker_example), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.TrackingSample) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.update_media_item_example), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.UpdatableSample) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.smooth_seeking_example), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.SmoothSeeking) } - ) - - HorizontalDivider() - - DemoListItemView( - title = stringResource(R.string.video_360), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.Video360) } - ) - - HorizontalDivider() + DemoListSectionView( + modifier = sectionModifier(miscDestinations.size), + ) { + miscDestinations.forEachIndexed { index, (label, destination) -> + DemoListItemView( + title = label, + modifier = itemModifier(index), + onClick = { navController.navigate(destination) } + ) - DemoListItemView( - title = stringResource(R.string.showcase_countdown), - modifier = itemModifier, - onClick = { navController.navigate(NavigationRoutes.CountdownShowcase) } - ) + if (index < miscDestinations.lastIndex) { + HorizontalDivider() + } + } } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt index ba1ea91aa..6b6c58ec6 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChapterShowcase.kt @@ -20,7 +20,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image @@ -39,14 +39,25 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.intl.LocaleList import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.MediaMetadata +import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.ui.player.PlayerView import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.ui.theme.paddings @@ -63,6 +74,7 @@ import kotlin.time.Duration.Companion.minutes @Composable fun ChapterShowcase(modifier: Modifier = Modifier) { val showCaseViewModel: ChaptersShowcaseViewModel = viewModel() + val demoItem = showCaseViewModel.demoItem val chapters by showCaseViewModel.chapters.collectAsState() val currentChapter by showCaseViewModel.currentChapter.collectAsState() val configuration = LocalConfiguration.current @@ -83,6 +95,7 @@ fun ChapterShowcase(modifier: Modifier = Modifier) { ) { ChapterList( chapters = chapters, + demoItem = demoItem, currentChapter = currentChapter, onChapterClick = showCaseViewModel::chapterClicked, ) @@ -95,6 +108,7 @@ private const val CurrentItemOffset = -64 @Composable private fun ChapterList( chapters: List, + demoItem: DemoItem, modifier: Modifier = Modifier, currentChapter: Chapter? = null, onChapterClick: (Chapter) -> Unit = {} @@ -107,16 +121,30 @@ private fun ChapterList( } } LazyRow( - modifier = modifier, + modifier = modifier.semantics { + collectionInfo = CollectionInfo(rowCount = chapters.size, columnCount = 1) + }, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.small), contentPadding = PaddingValues(MaterialTheme.paddings.small), state = state ) { - items(items = chapters, key = { it.id }) { chapter -> + itemsIndexed( + items = chapters, + key = { _, chapter -> chapter.id }, + ) { index, chapter -> ChapterItem( modifier = Modifier - .aspectRatio(16 / 9f), + .aspectRatio(16 / 9f) + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, chapter = chapter, + demoItem = demoItem, active = currentChapter == chapter, onClick = { onChapterClick(chapter) } ) @@ -128,6 +156,7 @@ private fun ChapterList( private fun ChapterItem( chapter: Chapter, active: Boolean, + demoItem: DemoItem, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -142,6 +171,8 @@ private fun ChapterItem( contentAlignment = Alignment.Center ) { val placeholder = rememberVectorPainter(image = Icons.Default.Image) + val localeList = demoItem.languageTag?.let { LocaleList(Locale(it)) } + AsyncImage( model = chapter.mediaMetadata.artworkUri, contentDescription = "", @@ -160,7 +191,11 @@ private fun ChapterItem( minLines = 2, textAlign = TextAlign.Start, overflow = TextOverflow.Ellipsis, - text = chapter.mediaMetadata.title.toString(), + text = buildAnnotatedString { + withStyle(SpanStyle(localeList = localeList)) { + append(chapter.mediaMetadata.title) + } + }, style = MaterialTheme.typography.bodySmall, fontWeight = if (active) FontWeight.Bold else null, color = Color.White, @@ -191,6 +226,7 @@ private fun ChapterItemPreview() { .build() ), active = false, + demoItem = DemoItem.OnDemandHorizontalVideo, onClick = {} ) } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt index 0c3679c2f..6433a1a3b 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/ChaptersShowcaseViewModel.kt @@ -32,6 +32,11 @@ class ChaptersShowcaseViewModel(application: Application) : AndroidViewModel(app */ val player: Player = PillarboxExoPlayer(application) + /** + * The media to play. + */ + val demoItem = DemoItem.OnDemandHorizontalVideo + /** * Progress tracker */ @@ -53,7 +58,7 @@ class ChaptersShowcaseViewModel(application: Application) : AndroidViewModel(app init { player.prepare() - player.setMediaItem(DemoItem.OnDemandHorizontalVideo.toMediaItem()) + player.setMediaItem(demoItem.toMediaItem()) } /** diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt index 82c3eb0f0..8fc064970 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt @@ -6,7 +6,6 @@ package ch.srgssr.pillarbox.demo.ui.showcases.playlists import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource @@ -17,6 +16,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material3.DropdownMenu @@ -38,6 +38,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.DpOffset import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.media3.common.Player @@ -91,6 +96,7 @@ fun CustomPlaybackSettingsShowcase( Row( modifier = Modifier + .semantics(mergeDescendants = true) {} .fillMaxWidth() .pointerInput(Unit) { detectTapGestures( @@ -130,6 +136,9 @@ fun CustomPlaybackSettingsShowcase( DropdownMenu( expanded = showRepeatModeMenu, onDismissRequest = { showRepeatModeMenu = false }, + modifier = Modifier.semantics { + collectionInfo = CollectionInfo(rowCount = repeatModes.size, columnCount = 1) + }, offset = menuOffset, ) { repeatModes.forEachIndexed { index, (repeatMode, repeatModeLabel) -> @@ -140,6 +149,14 @@ fun CustomPlaybackSettingsShowcase( player.repeatMode = repeatMode showRepeatModeMenu = false }, + modifier = Modifier.semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, leadingIcon = { AnimatedVisibility(index == selectedRepeatModeIndex) { Icon( @@ -156,7 +173,7 @@ fun CustomPlaybackSettingsShowcase( Row( modifier = Modifier .fillMaxWidth() - .clickable { + .toggleable(pauseAtEndOfItem) { pauseAtEndOfItem = !pauseAtEndOfItem player.pauseAtEndOfMediaItems = pauseAtEndOfItem } diff --git a/pillarbox-demo/src/main/res/values/strings.xml b/pillarbox-demo/src/main/res/values/strings.xml index a17c0cfdf..3e428ab75 100644 --- a/pillarbox-demo/src/main/res/values/strings.xml +++ b/pillarbox-demo/src/main/res/values/strings.xml @@ -5,6 +5,12 @@ Pillarbox Demo Playlists + Add to the playlist + Toggle shuffle + Clear playlist + Move up + Move down + Remove Layouts Story Simple From 3fbbb5b0ce4c182b519618e213ff202a8d9ac59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Thu, 5 Dec 2024 09:05:12 +0100 Subject: [PATCH 2/6] Fix build of the TV demo --- .../java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt index 589444173..fd4c5fd31 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/lists/ListsHome.kt @@ -146,6 +146,7 @@ fun ListsHome( val show = ContentList.LatestMediaForShow( urn = content.urn, show = content.title, + languageTag = contentList.languageTag, ) navController.navigate(show) @@ -154,7 +155,8 @@ fun ListsHome( is Content.Topic -> { val topic = ContentList.LatestMediaForTopic( urn = content.urn, - topic = content.title + topic = content.title, + languageTag = contentList.languageTag, ) navController.navigate(topic) From c51a6ab2fc842713dd9f35fba45dc54fc7845945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Tue, 10 Dec 2024 17:05:52 +0100 Subject: [PATCH 3/6] Make TalkBack inform about the selected item --- .../main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt | 2 ++ .../java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt | 6 +++++- .../ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt | 6 +++++- .../showcases/playlists/CustomPlaybackSettingsShowcase.kt | 6 +++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt index 0fcf41e9b..274b6cb75 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/MainNavigation.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.semantics.CollectionInfo import androidx.compose.ui.semantics.CollectionItemInfo import androidx.compose.ui.semantics.collectionInfo import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset @@ -239,6 +240,7 @@ private fun ListsMenu( isMenuVisible = false }, modifier = Modifier.semantics { + selected = isSelected collectionItemInfo = CollectionItemInfo( rowIndex = (index * servers.size) + envIndex, rowSpan = 1, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt index e86348032..30be08894 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/search/SearchHome.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.semantics.CollectionItemInfo import androidx.compose.ui.semantics.collectionInfo import androidx.compose.ui.semantics.collectionItemInfo import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -304,6 +305,8 @@ private fun SearchInput( ) ) { bus.forEachIndexed { index, bu -> + val isSelected = selectedBu == bu + DropdownMenuItem( text = { BuLabel(bu) }, onClick = { @@ -311,6 +314,7 @@ private fun SearchInput( showBuSelector = false }, modifier = Modifier.semantics { + selected = isSelected collectionItemInfo = CollectionItemInfo( rowIndex = index, rowSpan = 1, @@ -318,7 +322,7 @@ private fun SearchInput( columnSpan = 1, ) }, - trailingIcon = if (selectedBu == bu) { + trailingIcon = if (isSelected) { { Icon( imageVector = Icons.Default.Check, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt index 81801f031..b605ce3d0 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/settings/AppSettingsView.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.semantics.collectionInfo import androidx.compose.ui.semantics.collectionItemInfo import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset @@ -298,6 +299,8 @@ private fun DropdownSetting( offset = dropdownOffset, ) { entries.forEachIndexed { index, entry -> + val isSelected = entry == selectedEntry + DropdownMenuItem( text = { Text(text = entry.toString()) }, onClick = { @@ -305,6 +308,7 @@ private fun DropdownSetting( showDropdownMenu = false }, modifier = Modifier.semantics { + selected = isSelected collectionItemInfo = CollectionItemInfo( rowIndex = index, rowSpan = 1, @@ -313,7 +317,7 @@ private fun DropdownSetting( ) }, leadingIcon = { - AnimatedVisibility(entry == selectedEntry) { + AnimatedVisibility(isSelected) { Icon( imageVector = Icons.Default.Check, contentDescription = null, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt index 8fc064970..e4c4df3c8 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/playlists/CustomPlaybackSettingsShowcase.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.semantics.CollectionInfo import androidx.compose.ui.semantics.CollectionItemInfo import androidx.compose.ui.semantics.collectionInfo import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.DpOffset import androidx.lifecycle.compose.LifecycleResumeEffect @@ -142,6 +143,8 @@ fun CustomPlaybackSettingsShowcase( offset = menuOffset, ) { repeatModes.forEachIndexed { index, (repeatMode, repeatModeLabel) -> + val isSelected = index == selectedRepeatModeIndex + DropdownMenuItem( text = { Text(text = repeatModeLabel) }, onClick = { @@ -150,6 +153,7 @@ fun CustomPlaybackSettingsShowcase( showRepeatModeMenu = false }, modifier = Modifier.semantics { + selected = isSelected collectionItemInfo = CollectionItemInfo( rowIndex = index, rowSpan = 1, @@ -158,7 +162,7 @@ fun CustomPlaybackSettingsShowcase( ) }, leadingIcon = { - AnimatedVisibility(index == selectedRepeatModeIndex) { + AnimatedVisibility(isSelected) { Icon( imageVector = Icons.Default.Check, contentDescription = null, From 452a9b8fb5b1a5e3972fb2ca2a32915afb76acd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Thu, 5 Dec 2024 16:29:19 +0100 Subject: [PATCH 4/6] Improve accessibility in "Showcases" and on the player --- gradle/libs.versions.toml | 2 - pillarbox-demo-shared/build.gradle.kts | 3 + .../shared/ui/components/PillarboxSlider.kt | 51 +++++++++++++- .../player/metrics/StatsForNerdsViewModel.kt | 2 +- .../src/main/res/values/strings.xml | 2 +- pillarbox-demo-tv/build.gradle.kts | 1 + .../settings/PlaybackSettingsDrawer.kt | 2 +- pillarbox-demo/build.gradle.kts | 1 - .../demo/ui/player/DemoPlayerView.kt | 70 +++++-------------- .../ui/player/controls/PlayerBottomToolbar.kt | 49 ++++++------- .../ui/player/controls/PlayerTimeSlider.kt | 9 ++- .../settings/PlaybackSettingsContent.kt | 26 +++++-- .../player/settings/PlaybackSpeedSettings.kt | 28 ++++++-- .../player/settings/TrackSelectionSettings.kt | 48 +++++++++++-- .../showcases/misc/ResizablePlayerShowcase.kt | 16 ++++- .../showcases/misc/SmoothSeekingShowcase.kt | 18 +++-- .../showcases/misc/TrackingToggleShowcase.kt | 28 ++++++-- 17 files changed, 236 insertions(+), 120 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 172f13cfa..83a2530cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ android-gradle-plugin = "8.7.3" androidx-activity = "1.9.3" androidx-annotation = "1.9.1" androidx-compose = "2024.11.00" -androidx-compose-material-navigation = "1.7.0-beta01" # TODO Remove this once https://issuetracker.google.com/issues/347719428 is resolved androidx-core = "1.15.0" androidx-datastore = "1.1.1" androidx-fragment = "1.8.5" @@ -137,7 +136,6 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat androidx-compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" } androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } -androidx-compose-material-navigation = { module = "androidx.compose.material:material-navigation", version.ref = "androidx-compose-material-navigation" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } diff --git a/pillarbox-demo-shared/build.gradle.kts b/pillarbox-demo-shared/build.gradle.kts index 2ed86c8eb..937816ba4 100644 --- a/pillarbox-demo-shared/build.gradle.kts +++ b/pillarbox-demo-shared/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { api(libs.androidx.compose.ui.text) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.unit) + implementation(libs.androidx.core) + implementation(libs.androidx.core.ktx) api(libs.androidx.datastore.core) api(libs.androidx.datastore.preferences) api(libs.androidx.datastore.preferences.core) @@ -38,6 +40,7 @@ dependencies { api(libs.androidx.navigation.runtime) implementation(libs.androidx.paging.common) api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.datetime) api(libs.kotlinx.serialization.core) implementation(libs.okhttp) api(libs.srg.data) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt index 6bda61b55..1efc0db49 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt @@ -4,6 +4,7 @@ */ package ch.srgssr.pillarbox.demo.shared.ui.components +import android.view.accessibility.AccessibilityManager import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState @@ -12,6 +13,7 @@ import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.isSystemInDarkTheme @@ -21,11 +23,14 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -33,10 +38,15 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.core.content.getSystemService import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_inverseSurface import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_onSurface @@ -96,7 +106,12 @@ fun PillarboxSlider( PillarboxSliderInternal( activeTrackWeight = value / range.last.toFloat(), compactMode = compactMode, - modifier = modifier, + modifier = modifier.semantics { + progressBarRangeInfo = ProgressBarRangeInfo( + current = value.toFloat(), + range = range.first.toFloat()..range.last.toFloat(), + ) + }, secondaryValueWeight = secondaryValue?.let { it / range.last.toFloat() }, enabled = enabled, thumbColorEnabled = thumbColorEnabled, @@ -166,7 +181,12 @@ fun PillarboxSlider( PillarboxSliderInternal( activeTrackWeight = value / range.endInclusive, compactMode = compactMode, - modifier = modifier, + modifier = modifier.semantics { + progressBarRangeInfo = ProgressBarRangeInfo( + current = value.toFloat(), + range = range.start..range.endInclusive, + ) + }, secondaryValueWeight = secondaryValue?.let { it / range.endInclusive }, enabled = enabled, thumbColorEnabled = thumbColorEnabled, @@ -208,8 +228,11 @@ private fun PillarboxSliderInternal( onSeekBack: () -> Unit, onSeekForward: () -> Unit, ) { + val isTouchExplorationEnabled = rememberIsTouchExplorationEnabled() + val compactMode = compactMode && !isTouchExplorationEnabled val seekBarHeight by animateDpAsState(targetValue = if (compactMode) 8.dp else 16.dp, label = "seek_bar_height") val thumbColor by animateColorAsState(targetValue = if (enabled) thumbColorEnabled else thumbColorDisabled, label = "thumb_color") + val verticalPadding by animateDpAsState(targetValue = if (isTouchExplorationEnabled) 48.dp - seekBarHeight else 0.dp, label = "padding_top") val animatedActiveTrackWeight by animateFloatAsState(targetValue = activeTrackWeight, label = "active_track_weight") val activeTrackColor by animateColorAsState( @@ -230,6 +253,8 @@ private fun PillarboxSliderInternal( Row( modifier = modifier + .semantics(mergeDescendants = true) {} + .padding(vertical = verticalPadding / 2) .height(seekBarHeight) .then( if (interactionSource != null) { @@ -257,7 +282,7 @@ private fun PillarboxSliderInternal( Thumb( color = thumbColor, - enabled = enabled, + enabled = enabled && !isTouchExplorationEnabled, onSeekBack = onSeekBack, onSeekForward = onSeekForward, ) @@ -282,6 +307,26 @@ private fun PillarboxSliderInternal( } } +@Composable +private fun rememberIsTouchExplorationEnabled(): Boolean { + val accessibilityManager = LocalContext.current.getSystemService() ?: return false + val (isTouchExplorationEnabled, setIsTouchExplorationEnabled) = remember { + mutableStateOf(accessibilityManager.isTouchExplorationEnabled) + } + + DisposableEffect(Unit) { + val callback = AccessibilityManager.TouchExplorationStateChangeListener(setIsTouchExplorationEnabled) + + accessibilityManager.addTouchExplorationStateChangeListener(callback) + + onDispose { + accessibilityManager.removeTouchExplorationStateChangeListener(callback) + } + } + + return isTouchExplorationEnabled +} + private fun Modifier.clickToSlide( interactionSource: MutableInteractionSource, onSliderValueChange: (ratio: Float) -> Unit, diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt index 474b7b7d8..435962157 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt @@ -95,7 +95,7 @@ class StatsForNerdsViewModel(application: Application) : AndroidViewModel(applic getSessionInformation( labelRes = R.string.video_size, value = if (value.videoSize != VideoSize.UNKNOWN) { - "${value.videoSize.width}x${value.videoSize.height}" + "${value.videoSize.width}×${value.videoSize.height}" } else { null } diff --git a/pillarbox-demo-shared/src/main/res/values/strings.xml b/pillarbox-demo-shared/src/main/res/values/strings.xml index 7f5ab6d58..d976f6b4d 100644 --- a/pillarbox-demo-shared/src/main/res/values/strings.xml +++ b/pillarbox-demo-shared/src/main/res/values/strings.xml @@ -26,7 +26,7 @@ DRM loading Total load time Information - Session id + Session ID URI Playback duration Data volume diff --git a/pillarbox-demo-tv/build.gradle.kts b/pillarbox-demo-tv/build.gradle.kts index 892f6dba2..18e7caf88 100644 --- a/pillarbox-demo-tv/build.gradle.kts +++ b/pillarbox-demo-tv/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(libs.coil.network.okhttp) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.core) implementation(libs.okhttp) implementation(libs.srg.data) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt index cb1e4ebb8..0a8ed1153 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt @@ -383,7 +383,7 @@ private fun NavigationDrawerScope.TracksSetting( is VideoTrack -> { val text = buildString { append(format.width) - append("x") + append("×") append(format.height) if (format.bitrate > Format.NO_VALUE) { diff --git a/pillarbox-demo/build.gradle.kts b/pillarbox-demo/build.gradle.kts index 873c26083..4acc0fe82 100644 --- a/pillarbox-demo/build.gradle.kts +++ b/pillarbox-demo/build.gradle.kts @@ -58,7 +58,6 @@ dependencies { implementation(libs.androidx.compose.foundation.layout) implementation(libs.androidx.compose.material.icons.core) implementation(libs.androidx.compose.material.icons.extended) - implementation(libs.androidx.compose.material.navigation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3.window.size) implementation(libs.androidx.compose.runtime) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt index f5a908502..d3edfc26a 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt @@ -20,15 +20,13 @@ import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.navigation.ModalBottomSheetLayout -import androidx.compose.material.navigation.bottomSheet -import androidx.compose.material.navigation.rememberBottomSheetNavigator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,9 +37,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.Player -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettings import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettingsRepository import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettingsViewModel @@ -62,7 +57,7 @@ import ch.srgssr.pillarbox.ui.ScaleMode * @param pictureInPictureClick The picture in picture button action. If `null` no button. * @param displayPlaylist If it displays the playlist UI or not. */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalMaterial3Api::class) @Composable fun DemoPlayerView( player: Player, @@ -100,43 +95,22 @@ fun DemoPlayerView( } } } else { - val bottomSheetNavigator = rememberBottomSheetNavigator() - val navController = rememberNavController(bottomSheetNavigator) + var showSettingsSheet by remember { mutableStateOf(false) } - LaunchedEffect(bottomSheetNavigator.navigatorSheetState.isVisible) { - if (!bottomSheetNavigator.navigatorSheetState.isVisible) { - navController.popBackStack(route = RoutePlayer, false) - } - } - - ModalBottomSheetLayout( - modifier = modifier, - bottomSheetNavigator = bottomSheetNavigator, - ) { - NavHost(navController, startDestination = RoutePlayer) { - composable(route = RoutePlayer) { - PlayerContent( - player = player, - modifier = Modifier.fillMaxSize(), - pictureInPicture = pictureInPicture, - pictureInPictureClick = pictureInPictureClick, - displayPlaylist = displayPlaylist, - ) { - navController.navigate(route = RouteSettings) { - launchSingleTop = true - } - } - } - - bottomSheet(route = RouteSettings) { - LaunchedEffect(pictureInPicture) { - if (pictureInPicture) { - navController.popBackStack() - } - } + PlayerContent( + player = player, + modifier = Modifier.fillMaxSize(), + pictureInPicture = pictureInPicture, + pictureInPictureClick = pictureInPictureClick, + displayPlaylist = displayPlaylist, + optionClicked = { showSettingsSheet = true }, + ) - PlaybackSettingsContent(player = player) - } + if (showSettingsSheet) { + ModalBottomSheet( + onDismissRequest = { showSettingsSheet = false }, + ) { + PlaybackSettingsContent(player = player) } } } @@ -152,14 +126,11 @@ private fun PlayerContent( pictureInPicture: Boolean = false, pictureInPictureClick: (() -> Unit)? = null, displayPlaylist: Boolean = false, - optionClicked: (() -> Unit)? = null + optionClicked: () -> Unit, ) { var fullScreenState by remember { mutableStateOf(false) } - val fullScreenToggle: (Boolean) -> Unit = { fullScreenEnabled -> - fullScreenState = fullScreenEnabled - } val appSettings by appSettingsViewModel.currentAppSettings.collectAsStateWithLifecycle() ShowSystemUi(isShowed = !fullScreenState) Column(modifier = modifier) { @@ -200,7 +171,7 @@ private fun PlayerContent( ) { PlayerBottomToolbar( modifier = Modifier.fillMaxWidth(), - fullScreenClicked = fullScreenToggle, + fullScreenClicked = { fullScreenState = !fullScreenState }, fullScreenEnabled = fullScreenState, pictureInPictureClicked = pictureInPictureClick, optionClicked = optionClicked @@ -217,6 +188,3 @@ private fun PlayerContent( } } } - -private const val RoutePlayer = "player" -private const val RouteSettings = "settings" diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt index ec324e5e0..558bdb80b 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt @@ -12,7 +12,6 @@ import androidx.compose.material.icons.filled.PictureInPicture import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconToggleButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -32,9 +31,9 @@ import ch.srgssr.pillarbox.demo.shared.R fun PlayerBottomToolbar( fullScreenEnabled: Boolean, modifier: Modifier = Modifier, - fullScreenClicked: ((Boolean) -> Unit)? = null, - pictureInPictureClicked: (() -> Unit)? = null, - optionClicked: (() -> Unit)? = null + fullScreenClicked: () -> Unit, + pictureInPictureClicked: (() -> Unit)?, + optionClicked: () -> Unit, ) { Row(modifier = modifier) { pictureInPictureClicked?.let { @@ -46,33 +45,29 @@ fun PlayerBottomToolbar( ) } } - fullScreenClicked?.let { - IconToggleButton(checked = fullScreenEnabled, onCheckedChange = it) { - if (fullScreenEnabled) { - Icon( - tint = Color.White, - imageVector = Icons.Default.FullscreenExit, - contentDescription = "Exit full screen" - ) - } else { - Icon( - tint = Color.White, - imageVector = Icons.Default.Fullscreen, - contentDescription = "Open in full screen" - ) - } - } - } - optionClicked?.let { - IconButton( - onClick = it - ) { + + IconButton(onClick = fullScreenClicked) { + if (fullScreenEnabled) { + Icon( + tint = Color.White, + imageVector = Icons.Default.FullscreenExit, + contentDescription = "Exit fullscreen" + ) + } else { Icon( tint = Color.White, - imageVector = Icons.Default.Settings, - contentDescription = stringResource(R.string.settings) + imageVector = Icons.Default.Fullscreen, + contentDescription = "Enter fullscreen" ) } } + + IconButton(onClick = optionClicked) { + Icon( + tint = Color.White, + imageVector = Icons.Default.Settings, + contentDescription = stringResource(R.string.settings) + ) + } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt index 53b322864..ffa4686e6 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt @@ -21,6 +21,8 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.common.Timeline.Window @@ -44,6 +46,7 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds /** * Creates a [ProgressTrackerState] to track manual changes made to the current media being player. @@ -122,7 +125,11 @@ fun PlayerTimeSlider( value = currentProgressPercent, range = 0f..1f, compactMode = compactSlider, - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .semantics { + stateDescription = "${currentProgress.inWholeSeconds.seconds} of ${duration.inWholeSeconds.seconds}" + }, secondaryValue = bufferPercentage, enabled = availableCommands.canSeek(), thumbColorEnabled = Color.White, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt index f7a4dba56..905fead78 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt @@ -9,7 +9,7 @@ import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -21,7 +21,12 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.Player import androidx.navigation.compose.NavHost @@ -178,14 +183,19 @@ private fun SettingsHome( settings: List, settingsClicked: (SettingItem) -> Unit, ) { - LazyColumn { - items(items = settings) { setting -> + LazyColumn( + modifier = Modifier.semantics { + collectionInfo = CollectionInfo(rowCount = settings.size, columnCount = 1) + } + ) { + itemsIndexed(items = settings) { index, setting -> SettingsItem( modifier = Modifier.clickable( enabled = true, role = Role.Button, onClick = { settingsClicked(setting) } ), + index = index, title = setting.title, secondaryText = setting.subtitle, imageVector = setting.icon @@ -196,13 +206,21 @@ private fun SettingsHome( @Composable private fun SettingsItem( + index: Int, title: String, imageVector: ImageVector, modifier: Modifier = Modifier, secondaryText: String? = null ) { ListItem( - modifier = modifier, + modifier = modifier.semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, headlineContent = { Text(text = title) }, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt index f0729792f..5b0902dc0 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt @@ -15,6 +15,11 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import ch.srgssr.pillarbox.demo.shared.ui.player.settings.PlaybackSpeedSetting import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme @@ -33,14 +38,27 @@ fun PlaybackSpeedSettings( modifier: Modifier = Modifier, onSpeedSelected: (PlaybackSpeedSetting) -> Unit, ) { - LazyColumn(modifier) { + LazyColumn( + modifier = modifier.semantics { + collectionInfo = CollectionInfo(rowCount = playbackSpeeds.size, columnCount = 1) + }, + ) { itemsIndexed(items = playbackSpeeds) { index, playbackSpeed -> SettingsOptionItem( title = playbackSpeed.speed, enabled = playbackSpeed.isSelected, - modifier = Modifier.toggleable(playbackSpeed.isSelected) { - onSpeedSelected(playbackSpeed) - } + modifier = Modifier + .toggleable(playbackSpeed.isSelected) { + onSpeedSelected(playbackSpeed) + } + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + } ) if (index < playbackSpeeds.lastIndex) { @@ -57,7 +75,7 @@ private fun SettingsOptionItem(title: String, enabled: Boolean, modifier: Modifi headlineContent = { Text(text = title) }, trailingContent = { if (enabled) { - Icon(imageVector = Icons.Default.Check, contentDescription = "enabled") + Icon(imageVector = Icons.Default.Check, contentDescription = null) } } ) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt index a98f857d6..3cf801330 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.HearingDisabled import androidx.compose.material3.HorizontalDivider @@ -22,6 +22,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.media3.common.C import androidx.media3.common.Format @@ -56,12 +61,25 @@ fun TrackSelectionSettings( onTrackClick: (track: Track) -> Unit ) { val itemModifier = Modifier.fillMaxWidth() - LazyColumn(modifier = modifier) { + LazyColumn( + modifier = modifier.semantics { + // Adding 2 for the "Reset to default" and "Disabled" options + collectionInfo = CollectionInfo(rowCount = tracksSetting.tracks.size + 2, columnCount = 1) + }, + ) { item { ListItem( modifier = itemModifier .minimumInteractiveComponentSize() - .clickable { onResetClick() }, + .clickable { onResetClick() } + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = 0, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, headlineContent = { Text( text = stringResource(R.string.reset_to_default) @@ -72,7 +90,15 @@ fun TrackSelectionSettings( } item { SettingsOption( - modifier = itemModifier, + modifier = itemModifier + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = 1, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, selected = tracksSetting.disabled, onClick = onDisabledClick, content = { @@ -81,10 +107,18 @@ fun TrackSelectionSettings( ) HorizontalDivider() } - items(tracksSetting.tracks) { track -> + itemsIndexed(tracksSetting.tracks) { index, track -> val format = track.format SettingsOption( - modifier = itemModifier, + modifier = itemModifier + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index + 2, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, selected = track.isSelected, enabled = track.isSupported && !format.isForced(), onClick = { @@ -116,7 +150,7 @@ fun TrackSelectionSettings( is VideoTrack -> { val text = buildString { append(format.width) - append("x") + append("×") append(format.height) if (format.bitrate > Format.NO_VALUE) { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt index 2d3c5b1d9..dfa8656e1 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.Player @@ -79,8 +81,8 @@ private fun AdaptivePlayer(player: Player, modifier: Modifier = Modifier) { val (heightPercent, setHeightPercent) = remember { mutableFloatStateOf(1f) } BoxWithConstraints(modifier = modifier) { - val playerWidth by animateDpAsState(targetValue = maxWidth * widthPercent, label = "player_width") - val playerHeight by animateDpAsState(targetValue = maxHeight * heightPercent, label = "player_height") + val playerWidth by animateDpAsState(targetValue = this.maxWidth * widthPercent, label = "player_width") + val playerHeight by animateDpAsState(targetValue = this.maxHeight * heightPercent, label = "player_height") Box( modifier = Modifier.size(width = playerWidth, height = playerHeight), @@ -108,12 +110,14 @@ private fun AdaptivePlayer(player: Player, modifier: Modifier = Modifier) { SliderWithLabel( label = "W:", value = widthPercent, + hint = "Width", onValueChange = setWidthPercent, ) SliderWithLabel( label = "H:", value = heightPercent, + hint = "Height", onValueChange = setHeightPercent, ) @@ -138,15 +142,21 @@ private fun SliderWithLabel( modifier: Modifier = Modifier, label: String, value: Float, + hint: String, onValueChange: (Float) -> Unit ) { Row( - modifier = modifier.systemGestureExclusion(), + modifier = modifier + .semantics(mergeDescendants = true) {} + .systemGestureExclusion(), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.small), verticalAlignment = Alignment.CenterVertically, ) { Text( text = label, + modifier = Modifier.semantics { + contentDescription = hint + }, fontFamily = FontFamily.Monospace, ) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt index b0ace9eb8..5ee60dad1 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch @@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.Player import ch.srgssr.pillarbox.core.business.PillarboxExoPlayer @@ -96,19 +98,21 @@ fun SmoothSeekingShowcase() { } Row( modifier = Modifier + .semantics(mergeDescendants = true) {} .fillMaxWidth() - .padding(horizontal = MaterialTheme.paddings.baseline), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline), + .toggleable(smoothSeekingEnabled) { + smoothSeekingEnabled = it + } + .padding(MaterialTheme.paddings.baseline), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { + Text(text = stringResource(R.string.smooth_seeking_example)) + Switch( checked = smoothSeekingEnabled, - onCheckedChange = { enabled -> - smoothSeekingEnabled = enabled - } + onCheckedChange = null, ) - - Text(text = stringResource(id = R.string.smooth_seeking_example)) } } LifecycleStartEffect(Unit) { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt index c97e9d1c2..3e35bf01a 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt @@ -5,11 +5,13 @@ package ch.srgssr.pillarbox.demo.ui.showcases.misc import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -23,9 +25,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.semantics import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.demo.ui.player.PlayerView +import ch.srgssr.pillarbox.demo.ui.theme.paddings /** * Tracking toggle sample @@ -67,11 +71,23 @@ fun TrackingToggleShowcase() { modifier = playerModifier ) - Row(modifier = Modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically) { - Text(text = "Toggle tracking", color = MaterialTheme.colorScheme.onBackground) - Switch(checked = trackingEnabled, onCheckedChange = { - trackingEnabled = it - }) + Row( + modifier = Modifier + .semantics(mergeDescendants = true) {} + .fillMaxWidth() + .toggleable(trackingEnabled) { + trackingEnabled = it + } + .padding(MaterialTheme.paddings.baseline), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = "Toggle tracking") + + Switch( + checked = trackingEnabled, + onCheckedChange = null, + ) } } } From a111294022fcc2e5b0c9403bb3a34e1ec934bfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Fri, 6 Dec 2024 15:12:41 +0100 Subject: [PATCH 5/6] Remove Gradle cache from the `build_windows.yml` workflow --- .github/workflows/build_windows.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index bf5cefe95..d8da6056a 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -25,11 +25,6 @@ jobs: with: java-version: '17' distribution: 'temurin' - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - cache-read-only: true - name: Build project run: > ./gradlew From cbb58165d187d72657499e21e8f885457e01d9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Tue, 10 Dec 2024 20:00:16 +0100 Subject: [PATCH 6/6] - Replace media type emoji with labels. - Don't speak out metrics overlay. - Increase the height of the slider. - Speak out the slider progress as a percentage. - Add state label to the player control view. --- .../pillarbox/demo/shared/data/Playlist.kt | 3 +- .../pillarbox/demo/shared/ui/TalkBack.kt | 38 +++++++++++++++++++ .../shared/ui/components/PillarboxSlider.kt | 28 +------------- .../ui/player/metrics/MetricsOverlay.kt | 5 ++- .../src/main/res/values/strings.xml | 4 ++ .../demo/ui/components/ContentView.kt | 6 ++- .../pillarbox/demo/ui/player/PlayerView.kt | 13 ++++++- .../ui/player/controls/PlayerTimeSlider.kt | 9 +---- 8 files changed, 67 insertions(+), 39 deletions(-) create mode 100644 pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt index d941e125f..c621036ac 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt @@ -60,10 +60,11 @@ data class Playlist(val title: String, val items: List, val languageTa imageUri = "https://www.rts.ch/2022/08/18/12/38/13317144.image/16x9", languageTag = "fr-CH", ), + // urn:swi:video:48498670 DemoItem.URL( title = "Swiss wheelchair athlete wins top award", uri = "https://cdn.prod.swi-services.ch/video-projects/94f5f5d1-5d53-4336-afda-9198462c45d9/localised-videos/ENG/renditions/ENG.mp4", - description = "VOD - MP4 (urn:swi:video:48498670)", + description = "VOD - MP4", imageUri = "https://cdn.prod.swi-services.ch/video-delivery/images/94f5f5d1-5d53-4336-afda-9198462c45d9/_.1hAGinujJ.yERGrrGNzBGCNSxmhKZT/16x9", languageTag = "en-CH", ), diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt new file mode 100644 index 000000000..6e48ee580 --- /dev/null +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.shared.ui + +import android.view.accessibility.AccessibilityManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.getSystemService + +/** + * Remembers the current touch exploration state and provides it as a Composable state. + * + * @return A boolean indicating whether touch exploration is currently enabled. + */ +@Composable +fun rememberIsTouchExplorationEnabled(): Boolean { + val accessibilityManager = LocalContext.current.getSystemService() ?: return false + val (isTouchExplorationEnabled, setIsTouchExplorationEnabled) = remember { + mutableStateOf(accessibilityManager.isTouchExplorationEnabled) + } + + DisposableEffect(Unit) { + val callback = AccessibilityManager.TouchExplorationStateChangeListener(setIsTouchExplorationEnabled) + + accessibilityManager.addTouchExplorationStateChangeListener(callback) + + onDispose { + accessibilityManager.removeTouchExplorationStateChangeListener(callback) + } + } + + return isTouchExplorationEnabled +} diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt index 1efc0db49..45f71f773 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt @@ -4,7 +4,6 @@ */ package ch.srgssr.pillarbox.demo.shared.ui.components -import android.view.accessibility.AccessibilityManager import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState @@ -27,10 +26,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -38,7 +35,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.ProgressBarRangeInfo import androidx.compose.ui.semantics.progressBarRangeInfo import androidx.compose.ui.semantics.semantics @@ -46,8 +42,8 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import androidx.core.content.getSystemService import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent +import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTouchExplorationEnabled import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_inverseSurface import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_onSurface import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_primary @@ -254,7 +250,7 @@ private fun PillarboxSliderInternal( Row( modifier = modifier .semantics(mergeDescendants = true) {} - .padding(vertical = verticalPadding / 2) + .padding(vertical = verticalPadding) .height(seekBarHeight) .then( if (interactionSource != null) { @@ -307,26 +303,6 @@ private fun PillarboxSliderInternal( } } -@Composable -private fun rememberIsTouchExplorationEnabled(): Boolean { - val accessibilityManager = LocalContext.current.getSystemService() ?: return false - val (isTouchExplorationEnabled, setIsTouchExplorationEnabled) = remember { - mutableStateOf(accessibilityManager.isTouchExplorationEnabled) - } - - DisposableEffect(Unit) { - val callback = AccessibilityManager.TouchExplorationStateChangeListener(setIsTouchExplorationEnabled) - - accessibilityManager.addTouchExplorationStateChangeListener(callback) - - onDispose { - accessibilityManager.removeTouchExplorationStateChangeListener(callback) - } - } - - return isTouchExplorationEnabled -} - private fun Modifier.clickToSlide( interactionSource: MutableInteractionSource, onSliderValueChange: (ratio: Float) -> Unit, diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt index 46916e11d..72d5a5a55 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.tooling.preview.Preview import androidx.media3.common.Format import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions @@ -31,7 +32,9 @@ fun MetricsOverlay( ) { val currentVideoFormat = playbackMetrics.videoFormat val currentAudioFormat = playbackMetrics.audioFormat - Column(modifier = modifier) { + Column( + modifier = modifier.clearAndSetSemantics {}, + ) { currentVideoFormat?.let { OverlayText( overlayOptions = overlayOptions, diff --git a/pillarbox-demo-shared/src/main/res/values/strings.xml b/pillarbox-demo-shared/src/main/res/values/strings.xml index d976f6b4d..eb2e8ea4d 100644 --- a/pillarbox-demo-shared/src/main/res/values/strings.xml +++ b/pillarbox-demo-shared/src/main/res/values/strings.xml @@ -11,6 +11,10 @@ Search for content No results Enter something to search + Audio + Video + Controls visible + Controls hidden %1$d min Settings Audio tracks diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt index 782044350..5dec8fa5c 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt @@ -20,6 +20,7 @@ import ch.srg.dataProvider.integrationlayer.data.remote.Type import ch.srg.dataProvider.integrationlayer.data.remote.Vendor import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.Content +import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTouchExplorationEnabled import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import java.util.Date import kotlin.time.Duration.Companion.seconds @@ -78,9 +79,10 @@ private fun MediaView( languageTag: String? = null, onClick: () -> Unit ) { + val isTouchExplorationEnabled = rememberIsTouchExplorationEnabled() val mediaTypeIcon = when (content.mediaType) { - MediaType.AUDIO -> "🎧" - MediaType.VIDEO -> "🎬" + MediaType.AUDIO -> if (isTouchExplorationEnabled) "${stringResource(R.string.audio_content)} -" else "🎧" + MediaType.VIDEO -> if (isTouchExplorationEnabled) "${stringResource(R.string.video_content)} -" else "🎬" } val subtitlePrefix = if (content.showTitle != null) { "${content.showTitle} - " diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt index 36e11b690..0112777d5 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt @@ -19,8 +19,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.zIndex import androidx.media3.common.Player +import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerControls @@ -103,9 +107,16 @@ fun PlayerView( visible = controlsVisible ) val currentCredit by player.getCurrentCreditAsState() + val controlsStateDescription = if (visibilityState.isVisible) { + stringResource(R.string.controls_visible) + } else { + stringResource(R.string.controls_hidden) + } ToggleableBox( - modifier = modifier, + modifier = modifier.semantics { + stateDescription = controlsStateDescription + }, toggleable = controlsToggleable, visibilityState = visibilityState, toggleableContent = { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt index ffa4686e6..53b322864 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerTimeSlider.kt @@ -21,8 +21,6 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.stateDescription import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.common.Timeline.Window @@ -46,7 +44,6 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds /** * Creates a [ProgressTrackerState] to track manual changes made to the current media being player. @@ -125,11 +122,7 @@ fun PlayerTimeSlider( value = currentProgressPercent, range = 0f..1f, compactMode = compactSlider, - modifier = Modifier - .weight(1f) - .semantics { - stateDescription = "${currentProgress.inWholeSeconds.seconds} of ${duration.inWholeSeconds.seconds}" - }, + modifier = Modifier.weight(1f), secondaryValue = bufferPercentage, enabled = availableCommands.canSeek(), thumbColorEnabled = Color.White,