From 5dd18b06d44481cf79dd78f2772c4b345e48f1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 14 Oct 2024 10:32:20 +0200 Subject: [PATCH] Add SAM endpoints and CH/WW locations (#736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joaquim Stähli --- .../core/business/SRGMediaItemBuilder.kt | 42 +- .../integrationlayer/ResourceSelector.kt | 15 +- .../core/business/MediaItemUrnTest.kt | 127 ------ .../core/business/SRGMediaItemBuilderTest.kt | 236 +++++++++++ .../pillarbox/demo/shared/data/DemoItem.kt | 321 ++++++++------- .../pillarbox/demo/shared/data/Playlist.kt | 367 +++++++++--------- .../pillarbox/demo/shared/di/PlayerModule.kt | 66 +++- .../source/BlockedTimeRangeAssetLoader.kt | 6 +- .../shared/ui/examples/ExamplesViewModel.kt | 12 +- .../demo/tv/ui/examples/ExamplesHome.kt | 6 +- .../pillarbox/demo/tv/ui/lists/ListsHome.kt | 2 +- .../pillarbox/demo/tv/ui/search/SearchHome.kt | 6 +- .../srgssr/pillarbox/demo/MainNavigation.kt | 157 +++++--- .../demo/ui/examples/ExamplesHome.kt | 8 +- .../demo/ui/examples/InsertContentView.kt | 20 +- .../pillarbox/demo/ui/lists/ListsHome.kt | 19 +- .../demo/ui/player/SimplePlayerActivity.kt | 20 +- .../demo/ui/player/SimplePlayerViewModel.kt | 19 +- .../ui/player/playlist/MediaItemLibrary.kt | 2 +- 19 files changed, 870 insertions(+), 581 deletions(-) delete mode 100644 pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaItemUrnTest.kt create mode 100644 pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilderTest.kt diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilder.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilder.kt index 933f58313..9d786e6d9 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilder.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilder.kt @@ -22,16 +22,21 @@ class SRGMediaItemBuilder(mediaItem: MediaItem) { private val mediaItemBuilder = mediaItem.buildUpon() private var urn: String = mediaItem.mediaId private var host: URL = IlHost.DEFAULT + private var forceSAM: Boolean = false + private var forceLocation: String? = null private var vector: String = Vector.MOBILE init { urn = mediaItem.mediaId - mediaItem.localConfiguration?.uri?.let { uri -> + mediaItem.localConfiguration?.let { localConfiguration -> + val uri = localConfiguration.uri val urn = uri.lastPathSegment if (uri.toString().contains(PATH) && urn.isValidMediaUrn()) { uri.host?.let { hostname -> host = URL(Uri.Builder().scheme(host.protocol).authority(hostname).build().toString()) } this.urn = urn!! - uri.getQueryParameter("vector")?.let { vector = it } + this.forceSAM = uri.getQueryParameter(PARAM_FORCE_SAM)?.toBooleanStrictOrNull() ?: false + this.forceLocation = uri.getQueryParameter(PARAM_FORCE_LOCATION) + uri.getQueryParameter(PARAM_VECTOR)?.let { vector = it } } } } @@ -74,6 +79,28 @@ class SRGMediaItemBuilder(mediaItem: MediaItem) { return this } + /** + * Set force SAM + * + * @param forceSAM `true` to force the use of the SAM backend, `false` otherwise. + * @return this for convenience + */ + fun setForceSAM(forceSAM: Boolean): SRGMediaItemBuilder { + this.forceSAM = forceSAM + return this + } + + /** + * Set force location + * + * @param forceLocation The location to use on the IL/SAM backend calls. Can be `null`, `CH`, or `WW`. + * @return this for convenience + */ + fun setForceLocation(forceLocation: String?): SRGMediaItemBuilder { + this.forceLocation = forceLocation + return this + } + /** * Set vector * @@ -98,8 +125,17 @@ class SRGMediaItemBuilder(mediaItem: MediaItem) { val uri = Uri.Builder().apply { scheme(host.protocol) authority(host.host) + if (forceSAM) { + appendEncodedPath("sam") + } appendEncodedPath(PATH) appendEncodedPath(urn) + if (forceSAM) { + appendQueryParameter(PARAM_FORCE_SAM, true.toString()) + } + if (!forceLocation.isNullOrBlank()) { + appendQueryParameter(PARAM_FORCE_LOCATION, forceLocation) + } if (vector.isNotBlank()) { appendQueryParameter(PARAM_VECTOR, vector) } @@ -112,6 +148,8 @@ class SRGMediaItemBuilder(mediaItem: MediaItem) { private companion object { private const val PATH = "integrationlayer/2.1/mediaComposition/byUrn/" private const val PARAM_ONLY_CHAPTERS = "onlyChapters" + private const val PARAM_FORCE_SAM = "forceSAM" + private const val PARAM_FORCE_LOCATION = "forceLocation" private const val PARAM_VECTOR = "vector" } } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/ResourceSelector.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/ResourceSelector.kt index 5a1c7f59f..3504baac0 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/ResourceSelector.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/ResourceSelector.kt @@ -13,20 +13,15 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.data.Resource */ internal class ResourceSelector { /** - * Select the first resource from chapter that is playable by the Player. + * Select the first resource from [chapter] that is playable by the Player. * - * @param chapter + * @param chapter The [Chapter]. * @return null if no compatible resource is found. */ - @Suppress("SwallowedException") fun selectResourceFromChapter(chapter: Chapter): Resource? { - return try { - chapter.listResource?.first { - (it.type == Resource.Type.DASH || it.type == Resource.Type.HLS || it.type == Resource.Type.PROGRESSIVE) && - (it.drmList == null || it.drmList.any { drm -> drm.type == Drm.Type.WIDEVINE }) - } - } catch (e: NoSuchElementException) { - null + return chapter.listResource?.find { + (it.type == Resource.Type.DASH || it.type == Resource.Type.HLS || it.type == Resource.Type.PROGRESSIVE) && + (it.drmList.isNullOrEmpty() || it.drmList.any { drm -> drm.type == Drm.Type.WIDEVINE }) } } } diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaItemUrnTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaItemUrnTest.kt deleted file mode 100644 index eac236171..000000000 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaItemUrnTest.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.core.business - -import android.net.Uri -import androidx.core.net.toUri -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.test.ext.junit.runners.AndroidJUnit4 -import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost -import ch.srgssr.pillarbox.core.business.integrationlayer.service.Vector -import ch.srgssr.pillarbox.core.business.source.MimeTypeSrg -import org.junit.runner.RunWith -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -@RunWith(AndroidJUnit4::class) -class MediaItemUrnTest { - - @Test(expected = IllegalArgumentException::class) - fun `Check with invalid urn`() { - val urn = "urn:rts:show:3262363" - SRGMediaItemBuilder(urn).build() - } - - @Test(expected = IllegalArgumentException::class) - fun `Check with invalid mediaId`() { - SRGMediaItemBuilder(MediaItem.Builder().setMediaId("1234").build()).build() - } - - @Test - fun `Check default arguments`() { - val urn = "urn:rts:audio:3262363" - val mediaItem = SRGMediaItemBuilder(urn).build() - assertNotNull(mediaItem.localConfiguration) - assertEquals(Uri.parse(expectedUrl(urn)), mediaItem.localConfiguration?.uri) - assertEquals(MimeTypeSrg, mediaItem.localConfiguration?.mimeType) - assertEquals(urn, mediaItem.mediaId) - assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) - } - - @Test - fun `Check set MediaMetadata`() { - val urn = "urn:rts:audio:3262363" - val metadata = MediaMetadata.Builder() - .setTitle("Media title") - .setSubtitle("Media subtitle") - .setArtworkUri("Artwork uri".toUri()) - .build() - val mediaItem = SRGMediaItemBuilder(urn).apply { - setMediaMetadata(metadata) - }.build() - assertNotNull(mediaItem.localConfiguration) - assertEquals(Uri.parse(expectedUrl(urn)), mediaItem.localConfiguration?.uri) - assertEquals(MimeTypeSrg, mediaItem.localConfiguration?.mimeType) - assertEquals(urn, mediaItem.mediaId) - assertEquals(metadata, mediaItem.mediaMetadata) - } - - @Test - fun `Check set host to Stage`() { - val urn = "urn:rts:audio:3262363" - val mediaItem = SRGMediaItemBuilder(urn) - .setHost(IlHost.STAGE) - .build() - assertNotNull(mediaItem.localConfiguration) - assertEquals(Uri.parse(expectedUrl(urn, "il-stage.srgssr.ch")), mediaItem.localConfiguration?.uri) - assertEquals(MimeTypeSrg, mediaItem.localConfiguration?.mimeType) - assertEquals(urn, mediaItem.mediaId) - assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) - } - - @Test - fun `Check set vector to TV`() { - val urn = "urn:rts:audio:3262363" - val mediaItem = SRGMediaItemBuilder(urn) - .setVector(Vector.TV) - .build() - assertNotNull(mediaItem.localConfiguration) - assertEquals(Uri.parse(expectedUrl(urn, "il.srgssr.ch", vector = Vector.TV)), mediaItem.localConfiguration?.uri) - assertEquals(MimeTypeSrg, mediaItem.localConfiguration?.mimeType) - assertEquals(urn, mediaItem.mediaId) - assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) - } - - @Test - fun `Check uri from existing MediaItem`() { - val urn = "urn:rts:audio:3262363" - val inputMediaItem = MediaItem.Builder() - .setUri("https://il-stage.srgssr.ch/integrationlayer/2.1/mediaComposition/byUrn/$urn?vector=${Vector.TV}") - .build() - val mediaItem = SRGMediaItemBuilder(inputMediaItem).build() - assertNotNull(mediaItem.localConfiguration) - assertEquals(Uri.parse(expectedUrl(urn, "il-stage.srgssr.ch", vector = Vector.TV)), mediaItem.localConfiguration?.uri) - assertEquals(MimeTypeSrg, mediaItem.localConfiguration?.mimeType) - assertEquals(urn, mediaItem.mediaId) - assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) - } - - @Test - fun `Check uri from existing MediaItem changing parameters`() { - val urn = "urn:rts:audio:3262363" - val inputMediaItem = MediaItem.Builder() - .setUri("https://il-stage.srgssr.ch/integrationlayer/2.1/mediaComposition/byUrn/$urn?vector=${Vector.TV}") - .build() - val urn2 = "urn:rts:audio:123456" - val mediaItem = SRGMediaItemBuilder(inputMediaItem) - .setHost(IlHost.PROD) - .setVector(Vector.MOBILE) - .setUrn(urn2) - .build() - assertNotNull(mediaItem.localConfiguration) - assertEquals(Uri.parse(expectedUrl(urn2, "il.srgssr.ch", vector = Vector.MOBILE)), mediaItem.localConfiguration?.uri) - assertEquals(MimeTypeSrg, mediaItem.localConfiguration?.mimeType) - assertEquals(urn2, mediaItem.mediaId) - assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) - } - - companion object { - fun expectedUrl(urn: String, host: String = "il.srgssr.ch", vector: String = Vector.MOBILE): String { - return "https://$host/integrationlayer/2.1/mediaComposition/byUrn/$urn?vector=$vector&onlyChapters=true" - } - } -} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilderTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilderTest.kt new file mode 100644 index 000000000..3349d30de --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/SRGMediaItemBuilderTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business + +import android.net.Uri +import androidx.core.net.toUri +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.test.ext.junit.runners.AndroidJUnit4 +import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost +import ch.srgssr.pillarbox.core.business.integrationlayer.service.Vector +import ch.srgssr.pillarbox.core.business.source.MimeTypeSrg +import org.junit.runner.RunWith +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(AndroidJUnit4::class) +class SRGMediaItemBuilderTest { + + @Test(expected = IllegalArgumentException::class) + fun `Check with invalid urn`() { + val urn = "urn:rts:show:3262363" + SRGMediaItemBuilder(urn).build() + } + + @Test(expected = IllegalArgumentException::class) + fun `Check with empty mediaItem`() { + SRGMediaItemBuilder(MediaItem.Builder().build()).build() + } + + @Test(expected = IllegalArgumentException::class) + fun `Check with invalid mediaId`() { + SRGMediaItemBuilder(MediaItem.Builder().setMediaId("1234").build()).build() + } + + @Test + fun `Check default arguments`() { + val urn = "urn:rts:audio:3262363" + val mediaItem = SRGMediaItemBuilder(urn).build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check set MediaMetadata`() { + val urn = "urn:rts:audio:3262363" + val metadata = MediaMetadata.Builder() + .setTitle("Media title") + .setSubtitle("Media subtitle") + .setArtworkUri("Artwork uri".toUri()) + .build() + val mediaItem = SRGMediaItemBuilder(urn) + .setMediaMetadata(metadata) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(metadata, mediaItem.mediaMetadata) + } + + @Test + fun `Check set host to Stage`() { + val urn = "urn:rts:audio:3262363" + val ilHost = IlHost.STAGE + val mediaItem = SRGMediaItemBuilder(urn) + .setHost(ilHost) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(ilHost), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check set vector to TV`() { + val urn = "urn:rts:audio:3262363" + val vector = Vector.TV + val mediaItem = SRGMediaItemBuilder(urn) + .setVector(vector) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(vector = vector), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check no vector`() { + val urn = "urn:rts:audio:3262363" + val vector = "" + val mediaItem = SRGMediaItemBuilder(urn) + .setVector(vector) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(vector = vector), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check uri from existing MediaItem`() { + val urn = "urn:rts:audio:3262363" + val ilHost = IlHost.STAGE + val inputMediaItem = MediaItem.Builder() + .setUri("${ilHost}integrationlayer/2.1/mediaComposition/byUrn/$urn?vector=${Vector.TV}") + .build() + val mediaItem = SRGMediaItemBuilder(inputMediaItem).build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(ilHost, vector = Vector.TV), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check uri from existing MediaItem changing parameters`() { + val urn = "urn:rts:audio:3262363" + val inputMediaItem = MediaItem.Builder() + .setUri("https://il-stage.srgssr.ch/integrationlayer/2.1/mediaComposition/byUrn/$urn?vector=${Vector.TV}") + .build() + val urn2 = "urn:rts:audio:123456" + val mediaItem = SRGMediaItemBuilder(inputMediaItem) + .setHost(IlHost.PROD) + .setVector(Vector.MOBILE) + .setUrn(urn2) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn2.toIlUri(vector = Vector.MOBILE), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn2, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check set forceSAM`() { + val urn = "urn:rts:audio:3262363" + val ilHost = IlHost.STAGE + val forceSAM = true + val mediaItem = SRGMediaItemBuilder(urn) + .setHost(ilHost) + .setForceSAM(forceSAM) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(ilHost, forceSAM = forceSAM), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check set forceSAM from existing URL`() { + val urn = "urn:rts:audio:3262363" + val ilHost = IlHost.STAGE + val forceSAM = true + val inputMediaItem = MediaItem.Builder() + .setUri("${IlHost.PROD}sam/integrationlayer/2.1/mediaComposition/byUrn/$urn?forceSAM=true") + .build() + val mediaItem = SRGMediaItemBuilder(inputMediaItem) + .setHost(ilHost) + .setForceSAM(forceSAM) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(ilHost, forceSAM = forceSAM), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + @Test + fun `Check set forceLocation`() { + val urn = "urn:rts:audio:3262363" + val ilHost = IlHost.STAGE + val forceLocation = "CH" + val mediaItem = SRGMediaItemBuilder(urn) + .setHost(ilHost) + .setForceLocation(forceLocation) + .build() + val localConfiguration = mediaItem.localConfiguration + + assertNotNull(localConfiguration) + assertEquals(urn.toIlUri(ilHost, forceLocation = forceLocation), localConfiguration.uri) + assertEquals(MimeTypeSrg, localConfiguration.mimeType) + assertEquals(urn, mediaItem.mediaId) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + } + + companion object { + fun String.toIlUri( + host: URL = IlHost.DEFAULT, + vector: String = Vector.MOBILE, + forceSAM: Boolean = false, + forceLocation: String? = null, + ): Uri { + val samPath = if (forceSAM) "sam/" else "" + val queryParameters = listOfNotNull( + if (forceSAM) "forceSAM" to true else null, + if (forceLocation != null) "forceLocation" to forceLocation else null, + if (vector.isNotBlank()) "vector" to vector else null, + "onlyChapters" to true, + ).joinToString(separator = "&") { (name, value) -> + "$name=$value" + } + + return "${host}${samPath}integrationlayer/2.1/mediaComposition/byUrn/$this?$queryParameters".toUri() + } + } +} 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 d57eaa88e..d5c1a56bb 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 @@ -2,8 +2,6 @@ * Copyright (c) SRG SSR. All rights reserved. * License information is available from the LICENSE file. */ -@file:Suppress("MaximumLineLength", "MaxLineLength") - package ch.srgssr.pillarbox.demo.shared.data import android.net.Uri @@ -14,56 +12,52 @@ import androidx.media3.common.MediaMetadata import ch.srgssr.pillarbox.core.business.SRGMediaItemBuilder import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost import java.io.Serializable -import java.net.URL /** - * Demo item + * Generic media item that can represent either a content playable by URL or by URN. * - * @property title - * @property uri - * @property description - * @property imageUrl - * @property licenseUrl + * @property uri The URI of the media. + * @property title The title of the media + * @property description The optional description of the media. + * @property imageUri The optional image URI of the media. */ -@Suppress("UndocumentedPublicProperty") -data class DemoItem( - val title: String, - val uri: String, - val description: String? = null, - val imageUrl: String? = null, - val licenseUrl: String? = null, +sealed class DemoItem( + open val uri: String, + open val title: String?, + open val description: String?, + open val imageUri: String?, ) : Serializable { /** - * Convert to a [MediaItem] - * When [uri] is an URN, the [MediaItem] is created with [SRGMediaItemBuilder]. + * Represents a media item playable by URL. + * + * @property uri The URI of the media. + * @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 licenseUri The optional license URI of the media. */ - fun toMediaItem(ilHost: URL = IlHost.PROD): MediaItem { - return if (uri.startsWith("urn:")) { - SRGMediaItemBuilder(uri) - .setHost(ilHost) - .setMediaMetadata( - MediaMetadata.Builder() - .setTitle(title) - .setDescription(description) - .setArtworkUri(imageUrl?.let { Uri.parse(it) }) - .build() - ) - .build() - } else { + data class URL( + override val uri: String, + override val title: String? = null, + override val description: String? = null, + override val imageUri: String? = null, + val licenseUri: String? = null, + ) : DemoItem(uri, title, description, imageUri) { + override fun toMediaItem(): MediaItem { return MediaItem.Builder() .setUri(uri) - .setMediaId(this.uri) + .setMediaId(uri) .setMediaMetadata( MediaMetadata.Builder() .setTitle(title) .setDescription(description) - .setArtworkUri(imageUrl?.let { Uri.parse(it) }) + .setArtworkUri(imageUri?.let { Uri.parse(it) }) .build() ) .setDrmConfiguration( - licenseUrl?.let { + licenseUri?.let { DrmConfiguration.Builder(C.WIDEVINE_UUID) - .setLicenseUri(licenseUrl) + .setLicenseUri(licenseUri) .setMultiSession(true) .build() } @@ -72,388 +66,427 @@ data class DemoItem( } } - @Suppress("UndocumentedPublicClass") + /** + * Represents a media item playable by URN. + * + * @property urn The URN of the media. + * @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 host The host from which to load the media. + * @property forceSAM Whether to use SAM instead of the IL. + * @property forceLocation The optional location from which to load the media (either `CH`, `WW`, or `null`). + */ + data class URN( + val urn: String, + override val title: String? = null, + override val description: String? = null, + override val imageUri: String? = null, + val host: java.net.URL = IlHost.PROD, + val forceSAM: Boolean = false, + val forceLocation: String? = null, + ) : DemoItem(urn, title, description, imageUri) { + override fun toMediaItem(): MediaItem { + return SRGMediaItemBuilder(urn) + .setHost(host) + .setForceSAM(forceSAM) + .setForceLocation(forceLocation) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(title) + .setDescription(description) + .setArtworkUri(imageUri?.let { Uri.parse(it) }) + .build() + ) + .build() + } + } + + /** + * Converts this [DemoItem] into a [MediaItem]. + */ + abstract fun toMediaItem(): MediaItem + + @Suppress("MaximumLineLength", "MaxLineLength", "UndocumentedPublicClass", "UndocumentedPublicProperty") companion object { + @Suppress("ConstPropertyName") private const val serialVersionUID: Long = 1 - val OnDemandHLS = DemoItem( + val OnDemandHLS = URL( 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", - uri = "https://rts-vod-amd.akamaized.net/ww/14970442/7510ee63-05a4-3d48-8d26-1f1b3a82f6be/master.m3u8" ) - val ShortOnDemandVideoHLS = DemoItem( + 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", - uri = "https://rts-vod-amd.akamaized.net/ww/13317145/f1d49f18-f302-37ce-866c-1c1c9b76a824/master.m3u8" ) - val OnDemandVideoMP4 = DemoItem( + 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", - uri = "https://cdn.prod.swi-services.ch/video-projects/94f5f5d1-5d53-4336-afda-9198462c45d9/localised-videos/ENG/renditions/ENG.mp4" ) - val OnDemandVideoUHD = DemoItem( + 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", - uri = "https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8" ) - val LiveVideoHLS = DemoItem( + 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)", - uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0" ) - val DvrVideoHLS = DemoItem( + 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)", - uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8" ) - val LiveTimestampVideoHLS = DemoItem( + 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", - uri = "https://tagesschau.akamaized.net/hls/live/2020115/tagesschau/tagesschau_1/master.m3u8" ) - val OnDemandAudioMP3 = DemoItem( + 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", - uri = "https://rts-aod-dd.akamaized.net/ww/13306839/63cc2653-8305-3894-a448-108810b553ef.mp3" ) - val LiveAudioMP3 = DemoItem( + val LiveAudioMP3 = URL( title = "Audio livestream - MP3", + uri = "https://stream.srg-ssr.ch/m/couleur3/mp3_128", description = "Couleur 3 (live)", - uri = "https://stream.srg-ssr.ch/m/couleur3/mp3_128" ) - val DvrAudioHLS = DemoItem( + val DvrAudioHLS = URL( title = "Audio livestream - HLS", + uri = "https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8", description = "Couleur 3 (DVR)", - uri = "https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8" ) - val AppleBasic_4_3_HLS = DemoItem( + 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", - - uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" ) - val AppleBasic_16_9_TS_HLS = DemoItem( + 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", - - uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" ) - val AppleAdvanced_16_9_TS_HLS = DemoItem( + 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", - - uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8" ) - val AppleAdvanced_16_9_fMP4_HLS = DemoItem( + 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", - uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8" ) - val AppleAdvanced_16_9_HEVC_h264_HLS = DemoItem( + 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", - uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8" ) - val AppleAtmos = DemoItem( + val AppleAtmos = URL( title = "Apple Atmos", - uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8" + uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8", ) - val AppleWWDC_2023 = DemoItem( + val AppleWWDC_2023 = URL( title = "Apple WWDC Keynote 2023", - uri = "https://events-delivery.apple.com/0105cftwpxxsfrpdwklppzjhjocakrsk/m3u8/vod_index-PQsoJoECcKHTYzphNkXohHsQWACugmET.m3u8" + uri = "https://events-delivery.apple.com/0105cftwpxxsfrpdwklppzjhjocakrsk/m3u8/vod_index-PQsoJoECcKHTYzphNkXohHsQWACugmET.m3u8", ) - val AppleTvSample = DemoItem( + val AppleTvSample = URL( 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", - 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\n" ) - val GoogleDashH264 = DemoItem( + val GoogleDashH264 = URL( title = "VoD - Dash (H264)", uri = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" ) - val GoogleDashH264_CENC_Widewine = DemoItem( + val GoogleDashH264_CENC_Widewine = URL( title = "VoD - Dash Widewine cenc (H264)", uri = "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - licenseUrl = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" + licenseUri = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test", ) - val GoogleDashH265 = DemoItem( + val GoogleDashH265 = URL( title = "VoD - Dash (H265)", uri = "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" ) - val GoogleDashH265_CENC_Widewine = DemoItem( + val GoogleDashH265_CENC_Widewine = URL( title = "VoD - Dash widewine cenc (H265)", uri = "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", - licenseUrl = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" + licenseUri = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test", ) - val OnDemandHorizontalVideo = DemoItem( + val OnDemandHorizontalVideo = URN( title = "Horizontal video", - uri = "urn:rts:video:14827306" + urn = "urn:rts:video:14827306", ) - val OnDemandSquareVideo = DemoItem( + val OnDemandSquareVideo = URN( title = "Square video", - uri = "urn:rts:video:8393241" + urn = "urn:rts:video:8393241", ) - val OnDemandVerticalVideo = DemoItem( + val OnDemandVerticalVideo = URN( title = "Vertical video", - uri = "urn:rts:video:13444390" + urn = "urn:rts:video:13444390", ) - val TokenProtectedVideo = DemoItem( + val TokenProtectedVideo = URN( title = "Token-protected video", + urn = "urn:swisstxt:video:rts:c56ea781-99ad-40c3-8d9b-444cc5ac3aea", description = "Ski alpin, Slalom Messieurs", - uri = "urn:swisstxt:video:rts:c56ea781-99ad-40c3-8d9b-444cc5ac3aea" ) - val SuperfluouslyTokenProtectedVideo = DemoItem( + val SuperfluouslyTokenProtectedVideo = URN( title = "Superfluously token-protected video", + urn = "urn:rsi:video:15916771", description = "Telegiornale flash", - uri = "urn:rsi:video:15916771" ) - val DrmProtectedVideo = DemoItem( + val DrmProtectedVideo = URN( title = "DRM-protected video", + urn = "urn:rts:video:13639837", description = "Top Models 8870", - uri = "urn:rts:video:13639837" ) - val LiveVideo = DemoItem( + val LiveVideo = URN( title = "Live video", + urn = "urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651", description = "SRF 1", - uri = "urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651" ) - val DvrVideo = DemoItem( + val DvrVideo = URN( title = "DVR video livestream", + urn = "urn:rts:video:3608506", description = "RTS 1", - uri = "urn:rts:video:3608506" ) - val DvrAudio = DemoItem( + val DvrAudio = URN( title = "DVR audio livestream", + urn = "urn:rts:audio:3262363", description = "Couleur 3 (DVR)", - uri = "urn:rts:audio:3262363" ) - val OnDemandAudio = DemoItem( + 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", - uri = "urn:srf:audio:b9706015-632f-4e24-9128-5de074d98eda" ) - val Expired = DemoItem( + val Expired = URN( title = "Expired URN", + urn = "urn:rts:video:13382911", description = "Content that is not available anymore", - uri = "urn:rts:video:13382911" ) - val Unknown = DemoItem( + val Unknown = URN( title = "Unknown URN", + urn = "urn:srf:video:unknown", description = "Content that does not exist", - uri = "urn:srf:video:unknown" ) - val BitmovinOnDemandMultipleTracks = DemoItem( + 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", - uri = "https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8" ) - val BitmovinOnDemand_4K_HEVC = DemoItem( + val BitmovinOnDemand_4K_HEVC = URL( title = "4K, HEVC", uri = "https://cdn.bitmovin.com/content/encoding_test_dash_hls/4k/hls/4k_profile/master.m3u8" ) - val BitmovinOnDemandSingleAudio = DemoItem( + val BitmovinOnDemandSingleAudio = URL( title = "VoD, single audio track", uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8" ) - val BitmovinOnDemandAES128 = DemoItem( + val BitmovinOnDemandAES128 = URL( title = "AES-128", uri = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/m3u8s/11331.m3u8" ) - val BitmovinOnDemandProgressive = DemoItem( + val BitmovinOnDemandProgressive = URL( title = "AVC Progressive", uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/MI201109210084_mpeg-4_hd_high_1080p25_10mbits.mp4" ) - val UnifiedStreamingOnDemand_fMP4 = DemoItem( + 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" ) - val UnifiedStreamingOnDemandAlternateAudio = DemoItem( + 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" ) - val UnifiedStreamingOnDemandAudioOnly = DemoItem( + 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)" ) - val UnifiedStreamingOnDemandTrickplay = DemoItem( + 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" ) - val UnifiedStreamingOnDemandLimitedBandwidth = DemoItem( + 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" ) - val UnifiedStreamingOnDemandDynamicTrackSelection = DemoItem( + 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" ) - val UnifiedStreamingPureLive = DemoItem( + val UnifiedStreamingPureLive = URL( title = "Pure live", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8" ) - val UnifiedStreamingTimeshift = DemoItem( + val UnifiedStreamingTimeshift = URL( title = "Timeshift (5 minutes)", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?time_shift=300" ) - val UnifiedStreamingLiveAudio = DemoItem( + val UnifiedStreamingLiveAudio = URL( title = "Live audio", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?filter=(type!=%22video%22)" ) - val UnifiedStreamingPureLiveScte35 = DemoItem( + val UnifiedStreamingPureLiveScte35 = URL( title = "Pure live (scte35)", uri = "https://demo.unified-streaming.com/k8s/live/stable/scte35.isml/.m3u8" ) - val UnifiedStreamingOnDemand_fMP4_Clear = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_fMP4_HEVC_4K = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_MP4 = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_FragmentedMP4 = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_TrickPlay = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_TiledThumbnails = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_Accessibility = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_Single_TTML = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_Multiple_RFC_tags = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_Multiple_TTML = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_AudioOnly = DemoItem( + 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)" ) - val UnifiedStreamingOnDemand_Dash_Multiple_Audio_Codec = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_AlternateAudioLanguage = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_AccessibilityAudio = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_PureLive = DemoItem( + val UnifiedStreamingOnDemand_Dash_PureLive = URL( title = "Dash - Pure live", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd" ) - val UnifiedStreamingOnDemand_Dash_Timeshift = DemoItem( + 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" ) - val UnifiedStreamingOnDemand_Dash_DVB_LowLatency = DemoItem( + 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" ) - val BlockedSegment = DemoItem( + val BlockedSegment = URL( title = "Blocked segment at 29:26", uri = "urn:srf:video:40ca0277-0e53-4312-83e2-4710354ff53e", - imageUrl = "https://ws.srf.ch/asset/image/audio/f1a1ab5d-c009-4ba1-aae0-a2be5b89edd9/EPISODE_IMAGE/1465482801.png" + imageUri = "https://ws.srf.ch/asset/image/audio/f1a1ab5d-c009-4ba1-aae0-a2be5b89edd9/EPISODE_IMAGE/1465482801.png", ) - val OverlapinglockedSegments = DemoItem( + val OverlapinglockedSegments = URL( title = "Overlaping segments", uri = "urn:srf:video:d57f5c1c-080f-49a2-864e-4a1a83e41ae1", - imageUrl = "https://ws.srf.ch/asset/image/audio/75c3d4a4-4357-4703-b407-2d076aa15fd7/EPISODE_IMAGE/1384985072.png" + imageUri = "https://ws.srf.ch/asset/image/audio/75c3d4a4-4357-4703-b407-2d076aa15fd7/EPISODE_IMAGE/1384985072.png", ) - val MultiAudioWithAccessibility = DemoItem( + val MultiAudioWithAccessibility = URL( title = "Multi audio with AD track", description = "Bonjour la Suisse (5/5) - Que du bonheur?", uri = "urn:rts:video:8806923", - imageUrl = "https://www.rts.ch/2017/07/28/21/11/8806915.image/16x9" + imageUri = "https://www.rts.ch/2017/07/28/21/11/8806915.image/16x9", ) } } 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 e2855a611..5044a29c9 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 @@ -46,92 +46,92 @@ data class Playlist(val title: String, val items: List, val descriptio private val srgSsrStreamsUrls = Playlist( title = "SRG SSR streams (URLs)", items = listOf( - DemoItem( + DemoItem.URL( title = "Sacha part à la rencontre d'univers atypiques", uri = "https://rts-vod-amd.akamaized.net/ww/14970442/7510ee63-05a4-3d48-8d26-1f1b3a82f6be/master.m3u8", description = "VOD - HLS", - imageUrl = "https://www.rts.ch/2024/06/13/11/34/14970435.image/16x9" + imageUri = "https://www.rts.ch/2024/06/13/11/34/14970435.image/16x9", ), - DemoItem( + 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)", - imageUrl = "https://www.rts.ch/2022/08/18/12/38/13317144.image/16x9" + imageUri = "https://www.rts.ch/2022/08/18/12/38/13317144.image/16x9", ), - DemoItem( + 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)", - imageUrl = "https://cdn.prod.swi-services.ch/video-delivery/images/94f5f5d1-5d53-4336-afda-9198462c45d9/_.1hAGinujJ.yERGrrGNzBGCNSxmhKZT/16x9" + imageUri = "https://cdn.prod.swi-services.ch/video-delivery/images/94f5f5d1-5d53-4336-afda-9198462c45d9/_.1hAGinujJ.yERGrrGNzBGCNSxmhKZT/16x9", ), - DemoItem( + 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", - imageUrl = "https://img.rts.ch/audio/2010/image/924h3y-25865853.image?w=640&h=640" + imageUri = "https://img.rts.ch/audio/2010/image/924h3y-25865853.image?w=640&h=640", ), - DemoItem( + 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", - imageUrl = "https://il.srgssr.ch/images/?imageUrl=https%3A%2F%2Fwww.rts.ch%2F2020%2F05%2F18%2F14%2F20%2F11333286.image%2F16x9&format=jpg&width=960" + 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", ), - DemoItem( + 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", - imageUrl = "https://images.tagesschau.de/image/89045d82-5cd5-46ad-8f91-73911add30ee/AAABh3YLLz0/AAABibBx2rU/20x9-1280/tagesschau-logo-100.jpg" + imageUri = "https://images.tagesschau.de/image/89045d82-5cd5-46ad-8f91-73911add30ee/AAABh3YLLz0/AAABibBx2rU/20x9-1280/tagesschau-logo-100.jpg", ), - DemoItem( + DemoItem.URL( title = "On en parle", uri = "https://rts-aod-dd.akamaized.net/ww/13306839/63cc2653-8305-3894-a448-108810b553ef.mp3", description = "AOD - MP3", - imageUrl = "https://www.rts.ch/2023/09/28/17/49/11872957.image?w=624&h=351" + imageUri = "https://www.rts.ch/2023/09/28/17/49/11872957.image?w=624&h=351", ), - DemoItem( + DemoItem.URL( title = "Couleur 3 (live)", uri = "https://stream.srg-ssr.ch/m/couleur3/mp3_128", description = "Audio livestream - MP3", - imageUrl = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=640&h=640" + imageUri = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=640&h=640", ), - DemoItem( + DemoItem.URL( title = "Couleur 3 (DVR)", uri = "https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8", description = "Audio livestream - HLS", - imageUrl = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=640&h=640" - ) - ) + imageUri = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=640&h=640", + ), + ), ) private val srgSsrStreamsUrns = Playlist( title = "SRG SSR streams (URNs)", items = listOf( - DemoItem( + DemoItem.URN( title = "RTS 1", - uri = "urn:rts:video:3608506", + urn = "urn:rts:video:3608506", description = "DVR video livestream", - imageUrl = "https://www.rts.ch/2023/09/06/14/43/14253742.image/16x9" + imageUri = "https://www.rts.ch/2023/09/06/14/43/14253742.image/16x9", ), - DemoItem( + DemoItem.URN( title = "Couleur 3 (DVR)", - uri = "urn:rts:audio:3262363", + urn = "urn:rts:audio:3262363", description = "DVR audio livestream", - imageUrl = "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9" + imageUri = "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9", ), - DemoItem( + DemoItem.URN( title = "Telegiornale flash", - uri = "urn:rsi:video:15916771", + urn = "urn:rsi:video:15916771", description = "Superfluously token-protected video", - imageUrl = "https://il.rsi.ch/rsi-api/resize/image/v2/WEBVISUAL/256699/" + imageUri = "https://il.rsi.ch/rsi-api/resize/image/v2/WEBVISUAL/256699/", ), - DemoItem( + DemoItem.URN( title = "SRF 1", - uri = "urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651", + urn = "urn:srf:video:c4927fcf-e1a0-0001-7edd-1ef01d441651", description = "Live video", - imageUrl = "https://ws.srf.ch/asset/image/audio/d91bbe14-55dd-458c-bc88-963462972687/EPISODE_IMAGE" + imageUri = "https://ws.srf.ch/asset/image/audio/d91bbe14-55dd-458c-bc88-963462972687/EPISODE_IMAGE", ), - DemoItem( + DemoItem.URN( title = "Nachrichten von 08:00 Uhr - 08.03.2024", - uri = "urn:srf:audio:b9706015-632f-4e24-9128-5de074d98eda", + urn = "urn:srf:audio:b9706015-632f-4e24-9128-5de074d98eda", description = "On-demand audio stream" ), DemoItem.MultiAudioWithAccessibility, @@ -142,330 +142,330 @@ data class Playlist(val title: String, val items: List, val descriptio private val googleStreams = Playlist( title = "Google streams", items = listOf( - DemoItem( + DemoItem.URL( title = "VoD - Dash (H264)", uri = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "VoD - Dash Widewine cenc (H264)", uri = "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", - licenseUrl = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" + 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", ), - DemoItem( + DemoItem.URL( title = "VoD - Dash (H265)", uri = "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "VoD - Dash widewine cenc (H265)", uri = "https://storage.googleapis.com/wvmedia/cenc/hevc/tears/tears.mpd", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", - licenseUrl = "https://proxy.uat.widevine.com/proxy?video_id=2015_tears&provider=widevine_test" + 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", ) ) ) private val appleStreams = Playlist( title = "Apple streams", items = listOf( - DemoItem( + DemoItem.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", - imageUrl = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200" + imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", ), - DemoItem( + 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", - imageUrl = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200" + imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", ), - DemoItem( + 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", - imageUrl = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200" + imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", ), - DemoItem( + 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", - imageUrl = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200" + imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", ), - DemoItem( + 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", - imageUrl = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200" + imageUri = "https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200", ), - DemoItem( + DemoItem.URL( title = "Apple WWDC Keynote 2023", uri = "https://events-delivery.apple.com/0105cftwpxxsfrpdwklppzjhjocakrsk/m3u8/vod_index-PQsoJoECcKHTYzphNkXohHsQWACugmET.m3u8", - imageUrl = "https://www.apple.com/v/apple-events/home/ac/images/overview/recent-events/gallery/jun-2023__cjqmmqlyd21y_large_2x.jpg" + imageUri = "https://www.apple.com/v/apple-events/home/ac/images/overview/recent-events/gallery/jun-2023__cjqmmqlyd21y_large_2x.jpg", ), - DemoItem( + DemoItem.URL( title = "Apple Dolby Atmos", uri = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8", - imageUrl = "https://is1-ssl.mzstatic.com/image/thumb/-6farfCY0YClFd7-z_qZbA/1000x563.webp" + imageUri = "https://is1-ssl.mzstatic.com/image/thumb/-6farfCY0YClFd7-z_qZbA/1000x563.webp", ), - DemoItem( + 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", - imageUrl = "https://is1-ssl.mzstatic.com/image/thumb/cZUkXfqYmSy57DBI5TiTMg/1000x563.webp" + imageUri = "https://is1-ssl.mzstatic.com/image/thumb/cZUkXfqYmSy57DBI5TiTMg/1000x563.webp", ), - DemoItem( + 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", - imageUrl = "https://is1-ssl.mzstatic.com/image/thumb/IxmmS1rQ7ouO-pKoJsVpGw/1000x563.webp" + imageUri = "https://is1-ssl.mzstatic.com/image/thumb/IxmmS1rQ7ouO-pKoJsVpGw/1000x563.webp", ) ) ) private val thirdPartyStreams = Playlist( title = "Third-party streams", items = listOf( - DemoItem( + DemoItem.URL( title = "Brain Farm Skate Phantom Flex", uri = "https://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8", description = "4K video", - imageUrl = "https://i.ytimg.com/vi/d4_96ZWu3Vk/maxresdefault.jpg" + imageUri = "https://i.ytimg.com/vi/d4_96ZWu3Vk/maxresdefault.jpg", ) ) ) private val bitmovinStreams = Playlist( title = "Bitmovin streams streams", items = listOf( - DemoItem( + DemoItem.URL( title = "Multiple subtitles and audio tracks", uri = "https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8", - imageUrl = "https://durian.blender.org/wp-content/uploads/2010/06/05.8b_comp_000272.jpg" + imageUri = "https://durian.blender.org/wp-content/uploads/2010/06/05.8b_comp_000272.jpg", ), - DemoItem( + DemoItem.URL( title = "4K, HEVC", uri = "https://cdn.bitmovin.com/content/encoding_test_dash_hls/4k/hls/4k_profile/master.m3u8", - imageUrl = "https://peach.blender.org/wp-content/uploads/bbb-splash.png" + imageUri = "https://peach.blender.org/wp-content/uploads/bbb-splash.png", ), - DemoItem( + DemoItem.URL( title = "VoD, single audio track", uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8", - imageUrl = "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" + 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", ), - DemoItem( + DemoItem.URL( title = "AES-128", uri = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/m3u8s/11331.m3u8", - imageUrl = "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" + 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", ), - DemoItem( + DemoItem.URL( title = "AVC Progressive", uri = "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/MI201109210084_mpeg-4_hd_high_1080p25_10mbits.mp4", - imageUrl = "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" + 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", ) ) ) private val unifiedStreaming = Playlist( title = "Unified Streaming - HLS", items = listOf( - DemoItem( + DemoItem.URL( title = "Fragmented MP4", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "Key Rotation", uri = "https://demo.unified-streaming.com/k8s/keyrotation/stable/keyrotation/keyrotation.isml/.m3u8", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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)", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "Trickplay", uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.m3u8", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "Pure live", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + DemoItem.URL( title = "Timeshift (5 minutes)", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?time_shift=300", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + DemoItem.URL( title = "Live audio", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8?filter=(type!=%22video%22)", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + DemoItem.URL( title = "Pure live (scte35)", uri = "https://demo.unified-streaming.com/k8s/live/stable/scte35.isml/.m3u8", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + DemoItem.URL( title = "fMP4, clear", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel-fmp4.ism/.m3u8", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ) ) ) private val unifiedStreamingDash = Playlist( title = "Unified Streaming - Dash", items = listOf( - DemoItem( + DemoItem.URL( title = "MP4", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.mpd", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "Fragmented MP4", uri = "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.mpd", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "Trickplay", uri = "https://demo.unified-streaming.com/k8s/features/stable/no-handler-origin/tears-of-steel/tears-of-steel-trickplay.mpd", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + DemoItem.URL( title = "Pure live", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + DemoItem.URL( title = "Timeshift (5 minutes)", uri = "https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd?time_shift=300", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + DemoItem.URL( title = "DVB DASH low latency", uri = "https://demo.unified-streaming.com/k8s/live/stable/live-low-latency.isml/.mpd", - imageUrl = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png" + imageUri = "https://website-storage.unified-streaming.com/images/_1200x630_crop_center-center_none/default-facebook.png", ), - DemoItem( + 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)", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ), - DemoItem( + 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", - imageUrl = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg" + imageUri = "https://mango.blender.org/wp-content/gallery/4k-renders/01_thom_celia_bridge.jpg", ) ) ) private val aspectRatios = Playlist( title = "Aspect ratios", items = listOf( - DemoItem( + DemoItem.URN( title = "Horizontal video", - uri = "urn:rts:video:14827306", - imageUrl = "https://www.rts.ch/2024/04/10/19/23/14827621.image/16x9" + urn = "urn:rts:video:14827306", + imageUri = "https://www.rts.ch/2024/04/10/19/23/14827621.image/16x9", ), - DemoItem( + DemoItem.URN( title = "Square video", - uri = "urn:rts:video:8393241", - imageUrl = "https://www.rts.ch/2017/02/16/07/08/8393235.image/16x9" + urn = "urn:rts:video:8393241", + imageUri = "https://www.rts.ch/2017/02/16/07/08/8393235.image/16x9", ), - DemoItem( + DemoItem.URN( title = "Vertical video", - uri = "urn:rts:video:13444390", - imageUrl = "https://www.rts.ch/2022/10/06/17/32/13444380.image/4x5" + urn = "urn:rts:video:13444390", + imageUri = "https://www.rts.ch/2022/10/06/17/32/13444380.image/4x5", ) ) ) private val unbufferedStreams = Playlist( title = "Unbuffered streams", items = listOf( - DemoItem( + DemoItem.URL( title = "Couleur 3 en direct", uri = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0", description = "Live video (unbuffered)", - imageUrl = "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9" + imageUri = "https://www.rts.ch/2020/05/18/14/20/11333286.image/16x9", ), - DemoItem( + DemoItem.URL( title = "Couleur 3 en direct", uri = "https://stream.srg-ssr.ch/m/couleur3/mp3_128", description = "Audio livestream (unbuffered)", - imageUrl = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=320&h=320" + imageUri = "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=320&h=320", ) ) ) private val cornerCases = Playlist( title = "Corner cases", items = listOf( - DemoItem( + DemoItem.URN( title = "Expired URN", - uri = "urn:rts:video:13382911", + urn = "urn:rts:video:13382911", description = "Content that is not available anymore", - imageUrl = "https://www.rts.ch/2022/09/20/09/57/13365589.image/16x9" + imageUri = "https://www.rts.ch/2022/09/20/09/57/13365589.image/16x9", ), - DemoItem( + DemoItem.URN( title = "Unknown URN", - uri = "urn:srf:video:unknown", + urn = "urn:srf:video:unknown", description = "Content that does not exist" ), - DemoItem( + DemoItem.URN( title = "Custom MediaSource", - uri = "https://custom-media.ch/fondue", + urn = "https://custom-media.ch/fondue", description = "Using a custom CustomMediaSource" ), BlockedTimeRangeAssetLoader.DemoItemBlockedTimeRangeAtStartAndEnd, @@ -482,48 +482,45 @@ data class Playlist(val title: String, val items: List, val descriptio val VideoUrls = Playlist( title = "Video urls", items = listOf( - DemoItem( + DemoItem.URL( 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", - uri = "https://rts-vod-amd.akamaized.net/ww/13444390/f1b478f7-2ae9-3166-94b9-c5d5fe9610df/master.m3u8" ), - DemoItem( + 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", - uri = "https://rts-vod-amd.akamaized.net/ww/13444333/feb1d08d-e62c-31ff-bac9-64c0a7081612/master.m3u8" ), - DemoItem( + 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", - uri = "https://rts-vod-amd.akamaized.net/ww/13444466/2787e520-412f-35fb-83d7-8dbb31b5c684/master.m3u8" ), - DemoItem( + 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", - uri = "https://rts-vod-amd.akamaized.net/ww/13444447/c1d17174-ad2f-31c2-a084-846a9247fd35/master.m3u8" ), - DemoItem( + 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", - - uri = "https://rts-vod-amd.akamaized.net/ww/13444352/32145dc0-b5f8-3a14-ae11-5fc6e33aaaa4/master.m3u8" ), - DemoItem( + 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", - - uri = "https://rts-vod-amd.akamaized.net/ww/13444409/23f808a4-b14a-3d3e-b2ed-fa1279f6cf01/master.m3u8" ), - DemoItem( + 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", - - uri = "https://rts-vod-amd.akamaized.net/ww/13444371/3f26467f-cd97-35f4-916f-ba3927445920/master.m3u8" ), - DemoItem( + 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", - uri = "https://rts-vod-amd.akamaized.net/ww/13444428/857d97ef-0b8e-306e-bf79-3b13e8c901e4/master.m3u8" ) ) ) @@ -531,45 +528,45 @@ data class Playlist(val title: String, val items: List, val descriptio val VideoUrns = Playlist( title = "Video urns", items = listOf( - DemoItem( + DemoItem.URN( title = "Le R. - Légumes trop chers", + urn = "urn:rts:video:13444390", description = "Playlist item 1", - uri = "urn:rts:video:13444390" ), - DemoItem( + DemoItem.URN( title = "Le R. - Production de légumes bio", + urn = "urn:rts:video:13444333", description = "Playlist item 2", - uri = "urn:rts:video:13444333" ), - DemoItem( + DemoItem.URN( title = "Le R. - Endométriose", + urn = "urn:rts:video:13444466", description = "Playlist item 3", - uri = "urn:rts:video:13444466" ), - DemoItem( + DemoItem.URN( title = "Le R. - Prix Nobel de littérature 2022", + urn = "urn:rts:video:13444447", description = "Playlist item 4", - uri = "urn:rts:video:13444447" ), - DemoItem( + DemoItem.URN( title = "Le R. - Femme, vie, liberté", + urn = "urn:rts:video:13444352", description = "Playlist item 5", - uri = "urn:rts:video:13444352" ), - DemoItem( + DemoItem.URN( title = "Le R. - Attaque en Thailande", + urn = "urn:rts:video:13444409", description = "Playlist item 6", - uri = "urn:rts:video:13444409" ), - DemoItem( + DemoItem.URN( title = "Le R. - Douches et vestinaires non genrés", + urn = "urn:rts:video:13444371", description = "Playlist item 7", - uri = "urn:rts:video:13444371" ), - DemoItem( + DemoItem.URN( title = "Le R. - Prend soin de toi des autres et à demain", + urn = "urn:rts:video:13444428", description = "Playlist item 8", - uri = "urn:rts:video:13444428" ) ) ) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt index 5aef28462..c49df8179 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt @@ -16,13 +16,15 @@ import ch.srgssr.pillarbox.demo.shared.source.CustomAssetLoader import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository import ch.srgssr.pillarbox.player.PillarboxExoPlayer import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory +import okhttp3.Interceptor +import okhttp3.Response import java.net.URL +import ch.srg.dataProvider.integrationlayer.request.IlHost as DataProviderIlHost /** * Dependencies to make custom Dependency Injection */ object PlayerModule { - /** * Provide default player that allow to play urls and urns content from the SRG */ @@ -41,17 +43,65 @@ object PlayerModule { /** * Create il repository */ - fun createIlRepository(context: Context, ilHost: URL = IlHost.DEFAULT): ILRepository { + fun createIlRepository( + context: Context, + ilHost: URL = IlHost.DEFAULT, + forceSAM: Boolean = false, + ilLocation: String? = null, + ): ILRepository { val okHttp = OkHttpModule.createOkHttpClient(context) - val ilService = IlServiceModule.createIlService(okHttp, ilHost = providerIlHostFromUrl(ilHost)) + .newBuilder() + .addInterceptor(SamInterceptor(forceSAM)) + .addInterceptor(LocationInterceptor(ilLocation)) + .build() + val ilService = IlServiceModule.createIlService(okHttp, ilHost = ilHost.toDataProviderIlHost(forceSAM)) return ILRepository(dataProviderPaging = DataProviderPaging(ilService), ilService = ilService) } - private fun providerIlHostFromUrl(ilHost: URL): ch.srg.dataProvider.integrationlayer.request.IlHost { - return when (ilHost) { - IlHost.STAGE -> ch.srg.dataProvider.integrationlayer.request.IlHost.STAGE - IlHost.TEST -> ch.srg.dataProvider.integrationlayer.request.IlHost.TEST - else -> ch.srg.dataProvider.integrationlayer.request.IlHost.PROD + private fun URL.toDataProviderIlHost(forceSAM: Boolean): DataProviderIlHost { + return when (this) { + IlHost.PROD -> if (forceSAM) DataProviderIlHost.PROD_SAM else DataProviderIlHost.PROD + IlHost.STAGE -> if (forceSAM) DataProviderIlHost.STAGE_SAM else DataProviderIlHost.STAGE + IlHost.TEST -> if (forceSAM) DataProviderIlHost.TEST_SAM else DataProviderIlHost.TEST + else -> if (forceSAM) DataProviderIlHost.PROD_SAM else DataProviderIlHost.PROD + } + } + + private class SamInterceptor(private val forceSAM: Boolean) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + if (!forceSAM) { + return chain.proceed(request) + } + + val newUrl = request.url + .newBuilder() + .addQueryParameter("forceSAM", "true") + .build() + val newRequest = request.newBuilder() + .url(newUrl) + .build() + + return chain.proceed(newRequest) + } + } + + private class LocationInterceptor(private val location: String?) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + if (location.isNullOrBlank()) { + return chain.proceed(request) + } + + val newUrl = request.url + .newBuilder() + .addQueryParameter("forceLocation", location) + .build() + val newRequest = request.newBuilder() + .url(newUrl) + .build() + + return chain.proceed(newRequest) } } } 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 cc58a6630..c031e6570 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 @@ -90,7 +90,7 @@ class BlockedTimeRangeAssetLoader(context: Context) : AssetLoader(DefaultMediaSo /** * [DemoItem] to test [BlockedTimeRange] at start and end of the media. */ - val DemoItemBlockedTimeRangeAtStartAndEnd = DemoItem( + val DemoItemBlockedTimeRangeAtStartAndEnd = DemoItem.URL( 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", @@ -99,7 +99,7 @@ class BlockedTimeRangeAssetLoader(context: Context) : AssetLoader(DefaultMediaSo /** * [DemoItem] to test overlapping [BlockedTimeRange]. */ - val DemoItemBlockedTimeRangeOverlaps = DemoItem( + 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" @@ -108,7 +108,7 @@ class BlockedTimeRangeAssetLoader(context: Context) : AssetLoader(DefaultMediaSo /** * [DemoItem] to test included [BlockedTimeRange]. */ - val DemoItemBlockedTimeRangeIncluded = DemoItem( + 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" 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 1fe061e30..6b0001f5c 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 @@ -33,24 +33,24 @@ class ExamplesViewModel(application: Application) : AndroidViewModel(application .map { item -> val showTitle = item.show?.title.orEmpty() - DemoItem( + DemoItem.URN( title = if (showTitle.isNotBlank()) { "$showTitle (${item.title})" } else { item.title }, + urn = item.urn, description = "DRM-protected video", - imageUrl = item.imageUrl.rawUrl, - uri = item.urn + imageUri = item.imageUrl.rawUrl, ) } val listTokenProtectedContent = repository.getTvLiveCenter(Bu.RTS, PROTECTED_CONTENT_PAGE_SIZE).getOrDefault(emptyList()) .map { item -> - DemoItem( + DemoItem.URN( title = item.title, + urn = item.urn, description = "Token-protected video", - imageUrl = item.imageUrl.rawUrl, - uri = item.urn + imageUri = item.imageUrl.rawUrl, ) } val allProtectedContent = listDrmContent + listTokenProtectedContent diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt index d00ffef16..0be65f5c8 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/examples/ExamplesHome.kt @@ -122,9 +122,9 @@ fun ExamplesHome( } ) { item -> Box { - if (item.imageUrl != null) { + if (item.imageUri != null) { AsyncImage( - model = item.imageUrl, + model = item.imageUri, contentDescription = item.title, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop @@ -139,7 +139,7 @@ fun ExamplesHome( verticalArrangement = Arrangement.Bottom ) { Text( - text = item.title, + text = item.title ?: "No title", color = Color.White, overflow = TextOverflow.Ellipsis, maxLines = 2, 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 2e5a04a03..f51c24fd9 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 @@ -137,7 +137,7 @@ fun ListsHome( } is Content.Media -> { - val demoItem = DemoItem(title = content.title, uri = content.urn) + val demoItem = DemoItem.URN(title = content.title, urn = content.urn) PlayerActivity.startPlayer(context, demoItem) } diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt index 1337ec5fc..35c8a2ad7 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/search/SearchHome.kt @@ -91,11 +91,11 @@ fun SearchHome( searchViewModel.getScaledImageUrl(imageUrl, containerWidth) }, onItemClick = { item -> - val demoItem = DemoItem( + val demoItem = DemoItem.URN( title = item.title, - uri = item.urn, + urn = item.urn, description = item.description, - imageUrl = item.imageUrl.decorated(width = ImageWidth.W480) + imageUri = item.imageUrl.decorated(width = ImageWidth.W480), ) PlayerActivity.startPlayer(context, demoItem) 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 344b25512..756f3ff22 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 @@ -4,6 +4,7 @@ */ package ch.srgssr.pillarbox.demo +import android.content.Context import androidx.compose.animation.AnimatedContentScope import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -13,10 +14,10 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuDefaults import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold @@ -27,7 +28,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -93,6 +93,8 @@ fun MainNavigation() { val currentDestination = navBackStackEntry?.destination var ilHost by remember { mutableStateOf(IlHost.DEFAULT) } + var forceSAM by remember { mutableStateOf(false) } + var ilLocation by remember { mutableStateOf(null) } Scaffold( topBar = { @@ -115,11 +117,19 @@ fun MainNavigation() { } }, actions = { - if (currentDestination?.hasRoute(NavigationRoutes.ContentLists::class) == true) { - ListsMenu( - currentServer = ilHost, - onServerSelected = { ilHost = it } - ) + currentDestination?.let { currentDestination -> + if (currentDestination.hasRoute()) { + ListsMenu( + currentServer = ilHost, + currentForceSAM = forceSAM, + currentLocation = ilLocation, + onServerSelected = { host, forceSam, location -> + ilHost = host + forceSAM = forceSam + ilLocation = location + }, + ) + } } } ) @@ -129,6 +139,9 @@ fun MainNavigation() { } ) { innerPadding -> val context = LocalContext.current + val listsIlRepository = remember(ilHost, forceSAM, ilLocation) { + PlayerModule.createIlRepository(context, ilHost, forceSAM, ilLocation) + } NavHost(navController = navController, startDestination = NavigationRoutes.HomeSamples, modifier = Modifier.padding(innerPadding)) { composable(DemoPageView("home", listOf("app", "pillarbox", "examples"))) { @@ -140,9 +153,7 @@ fun MainNavigation() { } navigation(NavigationRoutes.ContentLists) { - val ilRepository = PlayerModule.createIlRepository(context, ilHost) - - listsNavGraph(navController, ilRepository, ilHost) + listsNavGraph(navController, listsIlRepository, ilHost, forceSAM, ilLocation) } composable(DemoPageView("home", listOf("app", "pillarbox", "settings"))) { @@ -158,11 +169,11 @@ fun MainNavigation() { val ilRepository = PlayerModule.createIlRepository(context) val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory(ilRepository)) SearchHome(searchViewModel = viewModel) { - val item = DemoItem( + val item = DemoItem.URN( title = it.title, - uri = it.urn, + urn = it.urn, description = it.description, - imageUrl = it.imageUrl.decorated(width = ImageWidth.W480) + imageUri = it.imageUrl.decorated(width = ImageWidth.W480), ) SimplePlayerActivity.startActivity(context, item) @@ -175,7 +186,9 @@ fun MainNavigation() { @Composable private fun ListsMenu( currentServer: URL, - onServerSelected: (server: URL) -> Unit + currentForceSAM: Boolean, + currentLocation: String?, + onServerSelected: (server: URL, forceSAM: Boolean, location: String?) -> Unit ) { var isMenuVisible by remember { mutableStateOf(false) } @@ -194,43 +207,95 @@ private fun ListsMenu( y = 0.dp, ), ) { + val context = LocalContext.current val currentServerUrl = currentServer.toString() - val servers = mapOf( - stringResource(R.string.production) to IlHost.PROD.toString(), - stringResource(R.string.stage) to IlHost.STAGE.toString(), - stringResource(R.string.test) to IlHost.TEST.toString() - ) + val servers = remember { getServers(context) } - Text( - text = stringResource(R.string.server), - modifier = Modifier - .padding(MenuDefaults.DropdownMenuItemContentPadding) - .align(Alignment.CenterHorizontally), - style = MaterialTheme.typography.labelMedium - ) + servers.forEachIndexed { index, (server, environmentConfig) -> + environmentConfig.forEach { config -> + val isSelected = currentServerUrl == config.host.toString() && + currentForceSAM == config.forceSAM && + currentLocation == config.location - servers.forEach { (name, url) -> - DropdownMenuItem( - text = { Text(text = name) }, - onClick = { - onServerSelected(URL(url)) - isMenuVisible = false - }, - trailingIcon = if (currentServerUrl == url) { - { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - } - } else { - null - } - ) + DropdownMenuItem( + text = { Text(text = "$server - ${config.name}") }, + onClick = { + onServerSelected(config.host, config.forceSAM, config.location) + isMenuVisible = false + }, + trailingIcon = if (isSelected) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + ) + } + } else { + null + }, + ) + } + + if (index < servers.lastIndex) { + HorizontalDivider( + modifier = Modifier.padding(vertical = MaterialTheme.paddings.small), + color = MaterialTheme.colorScheme.outline, + ) + } } } } +private fun getServers(context: Context): List>> { + val ilServers = listOf(null, "CH", "WW").map { location -> + val name = location?.let { "IL ($location)" } ?: "IL" + + name to listOf( + EnvironmentConfig( + name = context.getString(R.string.production), + host = IlHost.PROD, + location = location, + ), + EnvironmentConfig( + name = context.getString(R.string.stage), + host = IlHost.STAGE, + location = location, + ), + EnvironmentConfig( + name = context.getString(R.string.test), + host = IlHost.TEST, + location = location, + ), + ) + } + val samServer = "SAM" to listOf( + EnvironmentConfig( + name = context.getString(R.string.production), + host = IlHost.PROD, + forceSAM = true, + ), + EnvironmentConfig( + name = context.getString(R.string.stage), + host = IlHost.STAGE, + forceSAM = true, + ), + EnvironmentConfig( + name = context.getString(R.string.test), + host = IlHost.TEST, + forceSAM = true, + ) + ) + + return ilServers + samServer +} + +private data class EnvironmentConfig( + val name: String, + val host: URL, + val forceSAM: Boolean = false, + val location: String? = null, +) + @Composable private fun DemoBottomNavigation(navController: NavController, currentDestination: NavDestination?) { NavigationBar { @@ -255,7 +320,9 @@ private fun ListsMenuPreview() { PillarboxTheme { ListsMenu( currentServer = IlHost.PROD, - onServerSelected = {} + currentForceSAM = false, + currentLocation = null, + onServerSelected = { _, _, _ -> }, ) } } 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 323f97842..b725c4d95 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 @@ -78,7 +78,7 @@ private fun ListStreamView( DemoListSectionView { playlist.items.forEachIndexed { index, item -> DemoListItemView( - title = item.title, + title = item.title ?: "No title", modifier = Modifier.fillMaxWidth(), subtitle = item.description, onClick = { onItemClicked(item) }, @@ -99,9 +99,9 @@ private fun ListStreamPreview() { val playlist = Playlist( "Playlist title 1", listOf( - DemoItem(title = "Title 1", uri = "Uri 1"), - DemoItem(title = "Title 2", uri = "Uri 2"), - DemoItem(title = "Title 3", uri = "Uri 3"), + DemoItem.URL(title = "Title 1", uri = "Uri 1"), + DemoItem.URL(title = "Title 2", uri = "Uri 2"), + DemoItem.URL(title = "Title 3", uri = "Uri 3"), ) ) val playlists = listOf(playlist, playlist.copy(title = "Playlist title 2")) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/InsertContentView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/InsertContentView.kt index 1556d7ea4..491090d86 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/InsertContentView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/examples/InsertContentView.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaUrn import ch.srgssr.pillarbox.demo.R import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme @@ -45,11 +46,20 @@ private data class InsertContentData( get() = uri.startsWith("http") fun toDemoItem(): DemoItem { - return DemoItem( - title = uri, - uri = uri, - licenseUrl = licenseUrl - ) + return when { + isValidUrl -> DemoItem.URL( + title = uri, + uri = uri, + licenseUri = licenseUrl, + ) + + MediaUrn.isValid(uri) -> DemoItem.URN( + title = uri, + urn = uri, + ) + + else -> error("Invalid URI: $uri") + } } } 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 0c6e1e8f5..c02ef5834 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 @@ -42,7 +42,13 @@ private val defaultListsLevels = listOf("app", "pillarbox", "lists") /** * Build Navigation for integration layer list view */ -fun NavGraphBuilder.listsNavGraph(navController: NavController, ilRepository: ILRepository, ilHost: URL) { +fun NavGraphBuilder.listsNavGraph( + navController: NavController, + ilRepository: ILRepository, + ilHost: URL, + forceSAM: Boolean, + ilLocation: String?, +) { val contentClick = { contentList: ContentList, content: Content -> when (content) { is Content.Show -> { @@ -64,8 +70,15 @@ fun NavGraphBuilder.listsNavGraph(navController: NavController, ilRepository: IL } is Content.Media -> { - val item = DemoItem(title = content.title, uri = content.urn) - SimplePlayerActivity.startActivity(navController.context, item, ilHost) + val item = DemoItem.URN( + title = content.title, + urn = content.urn, + host = ilHost, + forceSAM = forceSAM, + forceLocation = ilLocation, + ) + + SimplePlayerActivity.startActivity(navController.context, item) } is Content.Channel -> { 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 d5cc620b6..316fce641 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 @@ -19,6 +19,7 @@ import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding @@ -31,12 +32,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.core.content.IntentCompat import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.media3.common.Player import ch.srgssr.pillarbox.analytics.SRGAnalytics -import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost import ch.srgssr.pillarbox.demo.DemoPageView import ch.srgssr.pillarbox.demo.service.DemoPlaybackService import ch.srgssr.pillarbox.demo.shared.data.DemoItem @@ -46,7 +45,6 @@ import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.player.service.PlaybackService import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import java.net.URL /** * Simple player activity that can handle picture in picture. @@ -59,12 +57,10 @@ import java.net.URL */ class SimplePlayerActivity : ComponentActivity(), ServiceConnection { - private lateinit var playerViewModel: SimplePlayerViewModel - private var layoutStyle: Int = LAYOUT_PLAYLIST + private val playerViewModel by viewModels() + private val layoutStyle by lazy { intent.getIntExtra(ARG_LAYOUT, LAYOUT_PLAYLIST) } private fun readIntent(intent: Intent) { - layoutStyle = intent.getIntExtra(ARG_LAYOUT, LAYOUT_PLAYLIST) - val playlist = IntentCompat.getSerializableExtra(intent, ARG_PLAYLIST, Playlist::class.java) playlist?.let { playerViewModel.playUri(it.items) } } @@ -77,8 +73,6 @@ class SimplePlayerActivity : ComponentActivity(), ServiceConnection { super.onCreate(savedInstanceState) - val ilHost = IntentCompat.getSerializableExtra(intent, ARG_IL_HOST, URL::class.java) ?: IlHost.DEFAULT - playerViewModel = ViewModelProvider(this, factory = SimplePlayerViewModel.Factory(application, ilHost))[SimplePlayerViewModel::class.java] readIntent(intent) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { lifecycleScope.launch { @@ -207,27 +201,25 @@ class SimplePlayerActivity : ComponentActivity(), ServiceConnection { companion object { private const val ARG_PLAYLIST = "ARG_PLAYLIST" private const val ARG_LAYOUT = "ARG_LAYOUT" - private const val ARG_IL_HOST = "ARG_IL_HOST" private const val LAYOUT_SIMPLE = 1 private const val LAYOUT_PLAYLIST = 0 /** * Start activity [SimplePlayerActivity] with [playlist] */ - fun startActivity(context: Context, playlist: Playlist, ilHost: URL = IlHost.DEFAULT) { + fun startActivity(context: Context, playlist: Playlist) { val layoutStyle: Int = if (playlist.items.isEmpty() || playlist.items.size > 1) LAYOUT_PLAYLIST else LAYOUT_SIMPLE val intent = Intent(context, SimplePlayerActivity::class.java) intent.putExtra(ARG_PLAYLIST, playlist) intent.putExtra(ARG_LAYOUT, layoutStyle) - intent.putExtra(ARG_IL_HOST, ilHost) context.startActivity(intent) } /** * Start activity [SimplePlayerActivity] with DemoItem. */ - fun startActivity(context: Context, item: DemoItem, ilHost: URL = IlHost.DEFAULT) { - startActivity(context, Playlist("UniqueItem", listOf(item)), ilHost) + fun startActivity(context: Context, item: DemoItem) { + startActivity(context, Playlist("UniqueItem", listOf(item))) } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt index b3cc86d62..89d8040f8 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt @@ -8,8 +8,6 @@ import android.app.Application import android.util.Log import android.util.Rational import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.media3.common.C import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException @@ -25,15 +23,11 @@ import ch.srgssr.pillarbox.player.asset.timeRange.Credit import ch.srgssr.pillarbox.player.extension.setHandleAudioFocus import ch.srgssr.pillarbox.player.extension.toRational import kotlinx.coroutines.flow.MutableStateFlow -import java.net.URL /** * Simple player view model than handle a PillarboxPlayer [player] */ -class SimplePlayerViewModel( - application: Application, - private val ilHost: URL -) : AndroidViewModel(application), PillarboxPlayer.Listener { +class SimplePlayerViewModel(application: Application) : AndroidViewModel(application), PillarboxPlayer.Listener { /** * Player as PillarboxPlayer */ @@ -76,7 +70,7 @@ class SimplePlayerViewModel( * @param items to play */ fun playUri(items: List) { - player.setMediaItems(items.map { it.toMediaItem(ilHost) }) + player.setMediaItems(items.map { it.toMediaItem() }) player.prepare() player.play() } @@ -161,13 +155,4 @@ class SimplePlayerViewModel( private companion object { private const val TAG = "PillarboxDemo" } - - /** - * Factory to create [SimplePlayerViewModel]. - */ - class Factory(private val application: Application, private val ilHost: URL) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return SimplePlayerViewModel(application, ilHost) as T - } - } } 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 9c762f70f..c8f26bd6f 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 @@ -135,7 +135,7 @@ private fun ItemList( ) Text( - text = item.title, + text = item.title ?: "No title", color = AlertDialogDefaults.textContentColor, ) }