Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

824 enforce official supported integration hostname #833

Merged
merged 16 commits into from
Dec 19, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
*/
package ch.srgssr.pillarbox.core.business

import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import ch.srgssr.pillarbox.core.business.integrationlayer.data.isValidMediaUrn
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlLocation
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlUrl
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlUrl.Companion.toIlUrl
import ch.srgssr.pillarbox.core.business.integrationlayer.service.Vector
import ch.srgssr.pillarbox.core.business.source.MimeTypeSrg
import ch.srgssr.pillarbox.player.PillarboxDsl
import ch.srgssr.pillarbox.player.source.PillarboxMediaSource
import java.net.URL

/**
* Creates a [MediaItem] suited for SRG SSR content identified by a URN.
Expand All @@ -29,7 +29,7 @@ import java.net.URL
*
* ```kotlin
* val mediaItem: MediaItem = SRGMediaItem("urn:rts:audio:3262363") {
* host(IlHost.Default)
* host(IlUrl.Default)
* vector(Vector.TV)
* }
* ```
Expand All @@ -50,6 +50,7 @@ import java.net.URL
@PillarboxDsl
@Suppress("FunctionName")
fun SRGMediaItem(urn: String, block: SRGMediaItemBuilder.() -> Unit = {}): MediaItem {
require(urn.isValidMediaUrn()) { "URN is not valid." }
return SRGMediaItemBuilder(MediaItem.Builder().setMediaId(urn).build()).apply(block).build()
}

Expand All @@ -60,7 +61,7 @@ fun SRGMediaItem(urn: String, block: SRGMediaItemBuilder.() -> Unit = {}): Media
* **Usage example**
* ```kotlin
* val mediaItem: MediaItem = sourceItem.buildUpon {
* host(IlHost.Stage)
* host(IlUrl.Stage)
* }
* ```
*
Expand All @@ -78,25 +79,19 @@ fun MediaItem.buildUpon(block: SRGMediaItemBuilder.() -> Unit): MediaItem {
class SRGMediaItemBuilder internal constructor(mediaItem: MediaItem) {
private val mediaItemBuilder = mediaItem.buildUpon()
private var urn: String = mediaItem.mediaId
private var host: URL = IlHost.DEFAULT
private var host: IlHost = IlHost.PROD
private var forceSAM: Boolean = false
private var ilLocation: IlLocation? = null
private var vector: Vector = Vector.MOBILE

init {
urn = mediaItem.mediaId
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!!
this.forceSAM = uri.getQueryParameter(PARAM_FORCE_SAM)?.toBooleanStrictOrNull() == true
this.ilLocation = uri.getQueryParameter(PARAM_FORCE_LOCATION)?.let { IlLocation.fromName(it) }
uri.getQueryParameter(PARAM_VECTOR)
?.let { Vector.fromLabel(it) }
?.let { vector = it }
}
val ilUrl = localConfiguration.uri.toIlUrl()
host = ilUrl.host
urn = ilUrl.urn
forceSAM = ilUrl.forceSAM
ilLocation = ilUrl.ilLocation
vector = ilUrl.vector
}
}

Expand Down Expand Up @@ -132,7 +127,7 @@ class SRGMediaItemBuilder internal constructor(mediaItem: MediaItem) {
*
* @param host The URL of the integration layer server.
*/
fun host(host: URL) {
fun host(host: IlHost) {
this.host = host
}

Expand Down Expand Up @@ -172,35 +167,10 @@ class SRGMediaItemBuilder internal constructor(mediaItem: MediaItem) {
* @return A new [MediaItem] ready for playback.
*/
fun build(): MediaItem {
require(urn.isValidMediaUrn()) { "Not a valid Urn!" }
mediaItemBuilder.setMediaId(urn)
val ilUrl = IlUrl(host = host, urn = urn, vector = vector, forceSAM = forceSAM, ilLocation = ilLocation)
mediaItemBuilder.setUri(ilUrl.uri)
mediaItemBuilder.setMediaId(ilUrl.urn)
mediaItemBuilder.setMimeType(MimeTypeSrg)
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())
}
ilLocation?.let {
appendQueryParameter(PARAM_FORCE_LOCATION, it.toString())
}
appendQueryParameter(PARAM_VECTOR, vector.toString())
appendQueryParameter(PARAM_ONLY_CHAPTERS, true.toString())
}.build()
mediaItemBuilder.setUri(uri)
return mediaItemBuilder.build()
}

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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,22 @@
package ch.srgssr.pillarbox.core.business.integrationlayer

import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost
import java.net.URL
import java.net.URLEncoder

/**
* Service used to get a scaled image URL. This only works for SRG images.
*
* @param baseUrl Base URL of the service.
* @param ilHost Base URL of the service.
*/
internal class ImageScalingService(
private val baseUrl: URL = IlHost.DEFAULT
private val ilHost: IlHost = IlHost.PROD
) {

fun getScaledImageUrl(
imageUrl: String,
): String {
val encodedImageUrl = URLEncoder.encode(imageUrl, Charsets.UTF_8.name())

return "${baseUrl}images/?imageUrl=$encodedImageUrl&format=webp&width=480"
return "${ilHost.baseHostUrl}/images/?imageUrl=$encodedImageUrl&format=webp&width=480"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,40 @@
*/
package ch.srgssr.pillarbox.core.business.integrationlayer.service

import java.net.URL

/**
* Object containing the different host URLs for the integration layer service.
* @property baseHostUrl The base hostname url.
*/
object IlHost {
enum class IlHost(val baseHostUrl: String) {

/**
* The base URL for the production environment.
*/
val PROD = URL("https://il.srgssr.ch/")
PROD(baseHostUrl = "https://il.srgssr.ch"),

/**
* The base URL for the test environment.
*/
val TEST = URL("https://il-test.srgssr.ch/")
TEST(baseHostUrl = "https://il-test.srgssr.ch"),

/**
* The base URL for the stage environment.
*/
val STAGE = URL("https://il-stage.srgssr.ch/")
STAGE(baseHostUrl = "https://il-stage.srgssr.ch"),

/**
* The default host used by the library.
*/
val DEFAULT = PROD
;

@Suppress("UndocumentedPublicClass")
companion object {

/**
* Parses the given [url] and returns the corresponding [IlHost].
*
* @param url The url to parse
* @return null if the [url] does not match any [IlHost].
*/
fun parse(url: String): IlHost? {
return entries.find { url.contains(it.baseHostUrl) }
MGaetan89 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.core.business.integrationlayer.service

import android.net.Uri
import ch.srgssr.pillarbox.core.business.integrationlayer.data.isValidMediaUrn

/**
* @property host the [IlHost] to use.
* @property urn the URN of the media to request.
* @property vector The [Vector] to use.
* @property forceSAM Force SAM usage.
* @property ilLocation the [IlLocation] of the request.
*/
data class IlUrl(
val host: IlHost,
val urn: String,
val vector: Vector,
val forceSAM: Boolean = false,
val ilLocation: IlLocation? = null,
) {

init {
require(urn.isValidMediaUrn())
}

/**
* Uri
*/
val uri: Uri = Uri.parse(host.baseHostUrl).buildUpon().apply {
if (forceSAM) {
appendEncodedPath("sam")
appendQueryParameter(PARAM_FORCE_SAM, true.toString())
}
appendEncodedPath(PATH)
appendEncodedPath(urn)
ilLocation?.let {
appendQueryParameter(PARAM_FORCE_LOCATION, it.toString())
}
appendQueryParameter(PARAM_VECTOR, vector.toString())
appendQueryParameter(PARAM_ONLY_CHAPTERS, true.toString())
}.build()

@Suppress("UndocumentedPublicClass")
companion object {
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"
private const val PATH = "integrationlayer/2.1/mediaComposition/byUrn/"

/**
* Convert an [Uri] into a valid [IlUrl].
*
* @return a [IlUrl] or throw an [IllegalArgumentException] if the Uri can't be parse.
*/
fun Uri.toIlUrl(): IlUrl {
StaehliJ marked this conversation as resolved.
Show resolved Hide resolved
val urn = lastPathSegment
val host = IlHost.parse(toString())
require(urn.isValidMediaUrn()) { "Invalid urn $urn found in $this" }
requireNotNull(host) { "Invalid url $this" }
val forceSAM = getQueryParameter(PARAM_FORCE_SAM)?.toBooleanStrictOrNull() == true || pathSegments.contains("sam")
val ilLocation = getQueryParameter(PARAM_FORCE_LOCATION)?.let { IlLocation.fromName(it) }
val vector = getQueryParameter(PARAM_VECTOR)?.let { Vector.fromLabel(it) } ?: Vector.MOBILE

return IlUrl(host = host, urn = checkNotNull(urn), vector, ilLocation = ilLocation, forceSAM = forceSAM)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Drm
import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition
import ch.srgssr.pillarbox.core.business.integrationlayer.data.Resource
import ch.srgssr.pillarbox.core.business.integrationlayer.data.isValidMediaUrn
import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlUrl.Companion.toIlUrl
import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionService
import ch.srgssr.pillarbox.core.business.tracker.SRGEventLoggerTracker
import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTracker
Expand Down Expand Up @@ -112,8 +112,9 @@ class SRGAssetLoader internal constructor(

override fun canLoadAsset(mediaItem: MediaItem): Boolean {
val localConfiguration = mediaItem.localConfiguration ?: return false

return localConfiguration.mimeType == MimeTypeSrg || localConfiguration.uri.lastPathSegment.isValidMediaUrn()
return localConfiguration.mimeType == MimeTypeSrg && runCatching {
localConfiguration.uri.toIlUrl()
}.isSuccess
}

override suspend fun loadAsset(mediaItem: MediaItem): Asset {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment
import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeInterval
import ch.srgssr.pillarbox.core.business.integrationlayer.data.TimeIntervalType
import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionService
import ch.srgssr.pillarbox.core.business.source.MimeTypeSrg
import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader
import ch.srgssr.pillarbox.core.business.source.SegmentAdapter
import ch.srgssr.pillarbox.core.business.source.TimeIntervalAdapter
Expand Down Expand Up @@ -51,6 +52,20 @@ class SRGAssetLoaderTest {
}
}

@Test
fun testCanLoadMediaItem() {
assertFalse(assetLoader.canLoadAsset(MediaItem.EMPTY))
assertFalse(
assetLoader.canLoadAsset(
MediaItem.Builder()
.setMimeType(MimeTypeSrg)
.setUri("https://fake.il/mediaComposition/urn:rts:video:123/path")
.build()
)
)
assertTrue(assetLoader.canLoadAsset(SRGMediaItem("urn:rts:video:123")))
}

@Test(expected = IllegalStateException::class)
fun testNoMediaId() = runTest {
assetLoader.loadAsset(MediaItem.Builder().build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlLocation
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
Expand Down Expand Up @@ -105,15 +104,14 @@ class SRGMediaItemBuilderTest {
@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}")
.setUri("https://il-stage.srgssr.ch/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(urn.toIlUri(IlHost.STAGE, vector = Vector.TV), localConfiguration.uri)
assertEquals(MimeTypeSrg, localConfiguration.mimeType)
assertEquals(urn, mediaItem.mediaId)
assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata)
Expand Down Expand Up @@ -164,7 +162,7 @@ class SRGMediaItemBuilderTest {
val ilHost = IlHost.STAGE
val forceSAM = true
val inputMediaItem = MediaItem.Builder()
.setUri("${IlHost.PROD}sam/integrationlayer/2.1/mediaComposition/byUrn/$urn?forceSAM=true")
.setUri("https://il-stage.srgssr.ch/sam/integrationlayer/2.1/mediaComposition/byUrn/$urn?forceSAM=true")
.build()
val mediaItem = SRGMediaItemBuilder(inputMediaItem).apply {
host(ilHost)
Expand Down Expand Up @@ -199,7 +197,7 @@ class SRGMediaItemBuilderTest {

companion object {
fun String.toIlUri(
host: URL = IlHost.DEFAULT,
host: IlHost = IlHost.PROD,
vector: Vector = Vector.MOBILE,
forceSAM: Boolean = false,
ilLocation: IlLocation? = null,
Expand All @@ -214,7 +212,7 @@ class SRGMediaItemBuilderTest {
"$name=$value"
}

return "${host}${samPath}integrationlayer/2.1/mediaComposition/byUrn/$this?$queryParameters".toUri()
return "${host.baseHostUrl}/${samPath}integrationlayer/2.1/mediaComposition/byUrn/$this?$queryParameters".toUri()
}
}
}
Loading
Loading