Skip to content

Commit

Permalink
824 enforce official supported integration hostname (#833)
Browse files Browse the repository at this point in the history
Co-authored-by: Gaëtan Muller <[email protected]>
  • Loading branch information
StaehliJ and MGaetan89 authored Dec 19, 2024
1 parent 2ee6651 commit e26644b
Show file tree
Hide file tree
Showing 15 changed files with 314 additions and 95 deletions.
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(IlHost.PROD)
* 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(IlHost.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 @@ -128,11 +123,11 @@ class SRGMediaItemBuilder internal constructor(mediaItem: MediaItem) {
}

/**
* Sets the host URL to the integration layer.
* Sets the host base URL to the integration layer.
*
* @param host The URL of the integration layer server.
* @param host The base 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)
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"
val ilUrl = IlUrl(host = host, urn = urn, vector = vector, forceSAM = forceSAM, ilLocation = ilLocation)
return mediaItemBuilder.setUri(ilUrl.uri)
.setMediaId(ilUrl.urn)
.setMimeType(MimeTypeSrg)
.build()
}
}
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,42 @@
*/
package ch.srgssr.pillarbox.core.business.integrationlayer.service

import java.net.URL

/**
* Object containing the different host URLs for the integration layer service.
* Represents the different host URLs for the integration layer service.
*
* @property baseHostUrl The base URL of the environment.
*/
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 The matching [IlHost] or `null` if none was found.
*/
fun parse(url: String): IlHost? {
return entries.find { url.startsWith(it.baseHostUrl) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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] representation of this [IlUrl].
*/
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()

internal 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/"

/**
* Converts an [Uri] into a valid [IlUrl].
*
* @return An [IlUrl] or throws an [IllegalArgumentException] if the [Uri] can't be parsed.
*/
internal fun Uri.toIlUrl(): IlUrl {
val urn = lastPathSegment
require(urn.isValidMediaUrn()) { "Invalid URN $urn found in $this" }
val host = IlHost.parse(toString())
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 = 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,28 @@ class SRGAssetLoaderTest {
}
}

@Test
fun testCanLoadAsset() {
assertTrue(assetLoader.canLoadAsset(SRGMediaItem("urn:rts:video:123")))
}

@Test
fun testCanLoadAsset_emptyMediaItem() {
assertFalse(assetLoader.canLoadAsset(MediaItem.EMPTY))
}

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

@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

0 comments on commit e26644b

Please sign in to comment.