diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7dedf56a1..51069c3dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,6 +66,7 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" } diff --git a/pillarbox-analytics/src/test/java/ch/srgssr/pillarbox/analytics/SRGAnalyticsTest.kt b/pillarbox-analytics/src/test/java/ch/srgssr/pillarbox/analytics/SRGAnalyticsTest.kt index 494c45c5b..676a6361f 100644 --- a/pillarbox-analytics/src/test/java/ch/srgssr/pillarbox/analytics/SRGAnalyticsTest.kt +++ b/pillarbox-analytics/src/test/java/ch/srgssr/pillarbox/analytics/SRGAnalyticsTest.kt @@ -60,7 +60,7 @@ class SRGAnalyticsTest { verifySequence { commandersAct.sendEvent(commandersActEvent) } - verify(exactly = 0) { + verify { comScore wasNot Called } } @@ -102,7 +102,7 @@ class SRGAnalyticsTest { verifySequence { comScore.getPersistentLabel(label) } - verify(exactly = 0) { + verify { commandersAct wasNot Called } } @@ -116,7 +116,7 @@ class SRGAnalyticsTest { verifySequence { commandersAct.getPermanentDataLabel(label) } - verify(exactly = 0) { + verify { comScore wasNot Called } } diff --git a/pillarbox-core-business/build.gradle.kts b/pillarbox-core-business/build.gradle.kts index 2ba8499de..8b9ed0c34 100644 --- a/pillarbox-core-business/build.gradle.kts +++ b/pillarbox-core-business/build.gradle.kts @@ -77,6 +77,11 @@ dependencies { api(libs.tagcommander.core) testImplementation(libs.junit) + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.ktor.client.mock) + testImplementation(libs.mockk) + testImplementation(libs.mockk.dsl) androidTestImplementation(project(":pillarbox-player-testutils")) diff --git a/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/LocalMediaCompositionDataSource.kt b/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/LocalMediaCompositionDataSource.kt index 352129b4d..a343fd578 100644 --- a/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/LocalMediaCompositionDataSource.kt +++ b/pillarbox-core-business/src/androidTest/java/ch/srgssr/pillarbox/core/business/LocalMediaCompositionDataSource.kt @@ -6,16 +6,15 @@ package ch.srgssr.pillarbox.core.business import android.content.Context import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition +import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultHttpClient import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionDataSource -import kotlinx.serialization.json.Json class LocalMediaCompositionDataSource(context: Context) : MediaCompositionDataSource { private val localData = HashMap() init { - val jsonSerializer = Json { ignoreUnknownKeys = true } val json = context.assets.open("media-compositions.json").bufferedReader().use { it.readText() } - val listMediaComposition: List = jsonSerializer.decodeFromString(json) + val listMediaComposition: List = DefaultHttpClient.jsonSerializer.decodeFromString(json) for (mediaComposition in listMediaComposition) { localData[mediaComposition.mainChapter.urn] = mediaComposition } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenDataSource.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenDataSource.kt index 5366e0170..ae6d7b545 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenDataSource.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenDataSource.kt @@ -5,7 +5,6 @@ package ch.srgssr.pillarbox.core.business.akamai import android.net.Uri -import android.text.TextUtils import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSpec import androidx.media3.datasource.DefaultHttpDataSource @@ -63,7 +62,7 @@ class AkamaiTokenDataSource private constructor( val queryParametersNames = uri.queryParameterNames val uriBuilder = uri.buildUpon().clearQuery().build().buildUpon() for (name in queryParametersNames) { - if (!TextUtils.equals(MediaCompositionMediaItemSource.TOKEN_QUERY_PARAM, name)) { + if (MediaCompositionMediaItemSource.TOKEN_QUERY_PARAM != name) { uriBuilder.appendQueryParameter(name, uri.getQueryParameter(name)) } } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenProvider.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenProvider.kt index 61f529f3a..152552a9f 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenProvider.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/akamai/AkamaiTokenProvider.kt @@ -37,7 +37,7 @@ class AkamaiTokenProvider(private val httpClient: HttpClient = DefaultHttpClient } private suspend fun getToken(acl: String): Result { - return Result.runCatching { + return runCatching { httpClient.get(TOKEN_SERVICE_URL) { url { appendEncodedPathSegments("akahd/token") diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonException.kt index a095f2fad..03c401479 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonException.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonException.kt @@ -5,6 +5,8 @@ package ch.srgssr.pillarbox.core.business.exception import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason +import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter +import ch.srgssr.pillarbox.core.business.integrationlayer.data.Segment import java.io.IOException /** diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/ResourceNotFoundException.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/ResourceNotFoundException.kt index 5cdeee3f3..6f83cceef 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/ResourceNotFoundException.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/exception/ResourceNotFoundException.kt @@ -4,10 +4,11 @@ */ package ch.srgssr.pillarbox.core.business.exception +import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter import java.io.IOException /** - * Resource not found exception is throw when : + * Resource not found exception is throw when: * - [Chapter] doesn't have a playable resource * - [Chapter.listResource] is empty or null */ diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/extension/BlockReason.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/extension/BlockReason.kt index c8c8ec17a..c265e2e9d 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/extension/BlockReason.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/extension/BlockReason.kt @@ -5,6 +5,7 @@ package ch.srgssr.pillarbox.core.business.extension import android.content.Context +import androidx.annotation.StringRes import ch.srgssr.pillarbox.core.business.R import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason @@ -23,6 +24,7 @@ fun Context.getString(blockReason: BlockReason): String { * * @return The android string resource id of a [BlockReason] */ +@StringRes fun BlockReason.getStringResId(): Int { return when (this) { BlockReason.AGERATING12 -> R.string.blockReason_ageRating12 diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultHttpClient.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultHttpClient.kt index 62f02e3f0..83f18d928 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultHttpClient.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultHttpClient.kt @@ -16,7 +16,7 @@ import okhttp3.logging.HttpLoggingInterceptor * Default ktor HttpClient. */ object DefaultHttpClient { - private val jsonSerializer = Json { + internal val jsonSerializer = Json { encodeDefaults = true ignoreUnknownKeys = true isLenient = true diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultMediaCompositionDataSource.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultMediaCompositionDataSource.kt index 160b6ffae..71032dc09 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultMediaCompositionDataSource.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultMediaCompositionDataSource.kt @@ -26,7 +26,7 @@ class DefaultMediaCompositionDataSource( ) : MediaCompositionDataSource { override suspend fun getMediaCompositionByUrn(urn: String): Result { - return Result.runCatching { + return runCatching { httpClient.get(baseUrl) { url { appendEncodedPathSegments("integrationlayer/2.1/mediaComposition/byUrn") diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/TotalPlaytimeCounter.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/TotalPlaytimeCounter.kt index 5afc78128..d52962ea0 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/TotalPlaytimeCounter.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/TotalPlaytimeCounter.kt @@ -8,9 +8,13 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds /** - * Total playtime counter + * Total playtime counter. + * + * @param timeProvider A callback invoked whenever the current time is needed. */ -class TotalPlaytimeCounter { +class TotalPlaytimeCounter( + private val timeProvider: () -> Long = { System.currentTimeMillis() }, +) { private var totalPlayTime: Duration = Duration.ZERO private var lastPlayTime = 0L @@ -20,7 +24,7 @@ class TotalPlaytimeCounter { */ fun play() { pause() - lastPlayTime = System.currentTimeMillis() + lastPlayTime = timeProvider() } /** @@ -32,7 +36,7 @@ class TotalPlaytimeCounter { return if (lastPlayTime <= 0L) { totalPlayTime } else { - totalPlayTime + (System.currentTimeMillis() - lastPlayTime).milliseconds + totalPlayTime + (timeProvider() - lastPlayTime).milliseconds } } @@ -41,7 +45,7 @@ class TotalPlaytimeCounter { */ fun pause() { if (lastPlayTime > 0L) { - totalPlayTime += (System.currentTimeMillis() - lastPlayTime).milliseconds + totalPlayTime += (timeProvider() - lastPlayTime).milliseconds lastPlayTime = 0L } } 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 new file mode 100644 index 000000000..3acfdd242 --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaItemUrnTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business + +import androidx.core.net.toUri +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class MediaItemUrnTest { + @Test + fun `MediaItemUrn with all parameters`() { + val urn = "urn:rts:audio:3262363" + val title = "Media title" + val subtitle = "Media subtitle" + val artworkUri = "Artwork uri".toUri() + val mediaItem = MediaItemUrn( + urn = urn, + title = title, + subtitle = subtitle, + artworkUri = artworkUri, + ) + + assertEquals(urn, mediaItem.mediaId) + assertEquals(title, mediaItem.mediaMetadata.title) + assertEquals(subtitle, mediaItem.mediaMetadata.subtitle) + assertEquals(artworkUri, mediaItem.mediaMetadata.artworkUri) + } + + @Test + fun `MediaItemUrn with urn only`() { + val urn = "urn:rts:audio:3262363" + val mediaItem = MediaItemUrn( + urn = urn, + ) + + assertEquals(urn, mediaItem.mediaId) + assertNull(mediaItem.mediaMetadata.title) + assertNull(mediaItem.mediaMetadata.subtitle) + assertNull(mediaItem.mediaMetadata.artworkUri) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaUrnTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaUrnTest.kt deleted file mode 100644 index 9ec656439..000000000 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/MediaUrnTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.core.business - -import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaUrn -import ch.srgssr.pillarbox.core.business.integrationlayer.data.isValidMediaUrn -import org.junit.Assert -import org.junit.Test - -class MediaUrnTest { - - @Test - fun testEmpty() { - val urn = "" - Assert.assertFalse(urn.isValidMediaUrn()) - } - - @Test - fun testNull() { - val urn: String? = null - Assert.assertFalse(urn.isValidMediaUrn()) - } - - @Test - fun testVideoUrn() { - val urn = "urn:rts:video:123345" - Assert.assertTrue(urn.isValidMediaUrn()) - } - - @Test - fun testAudioUrn() { - val urn = "urn:rts:audio:123345" - Assert.assertTrue(MediaUrn.isValid(urn)) - } - - @Test - fun testRandomText() { - val urn = "hello guys!" - Assert.assertFalse(urn.isValidMediaUrn()) - } - - @Test - fun testHttps() { - val urn = "https://www.rts.ch/media" - Assert.assertFalse(urn.isValidMediaUrn()) - } - - @Test - fun testHttpsWithUrn() { - val urn = "https://www.rts.ch/media/urn:rts:video:123345" - Assert.assertFalse(urn.isValidMediaUrn()) - } - - @Test - fun testShowUrn() { - val urn = "urn:rts:show:tv:1234" - Assert.assertFalse(MediaUrn.isValid(urn)) - } - - @Test - fun testChannelUrn() { - val urn = "urn:rts:channel:tv:1234" - Assert.assertFalse(MediaUrn.isValid(urn)) - } -} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/TestJsonSerialization.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/TestJsonSerialization.kt index a3e2b0251..958dd8fcb 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/TestJsonSerialization.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/TestJsonSerialization.kt @@ -7,15 +7,12 @@ package ch.srgssr.pillarbox.core.business import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition +import ch.srgssr.pillarbox.core.business.integrationlayer.service.DefaultHttpClient.jsonSerializer import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json import org.junit.Assert import org.junit.Test class TestJsonSerialization { - - private val jsonSerializer = Json { ignoreUnknownKeys = true } - @Test fun testChapterValidJson() { val json = "{\"urn\":\"urn:srf:video:12343\",\"title\":\"Chapter title\",\"imageUrl\":\"https://image.png\",\"blockReason\": \"UNKNOWN\"}" diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonExceptionTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonExceptionTest.kt new file mode 100644 index 000000000..de4918657 --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/exception/BlockReasonExceptionTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.exception + +import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason +import kotlin.test.Test +import kotlin.test.assertEquals + +class BlockReasonExceptionTest { + @Test + fun `BlockReasonException created with a BlockReason`() { + BlockReason.entries.forEach { blockReason -> + val exception = BlockReasonException(blockReason) + + assertEquals(blockReason.name, exception.message) + } + } + + @Test + fun `BlockReasonException created with a message matching a BlockReason`() { + BlockReason.entries.forEach { blockReason -> + val exception = BlockReasonException(blockReason.name) + + assertEquals(blockReason.name, exception.message) + } + } + + @Test + fun `BlockReasonException created with a message not matching a BlockReason`() { + val exception = BlockReasonException("FOO_BAR") + + assertEquals(BlockReason.UNKNOWN.name, exception.message) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/extension/BlockReasonTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/extension/BlockReasonTest.kt new file mode 100644 index 000000000..c9b814c61 --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/extension/BlockReasonTest.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.extension + +import ch.srgssr.pillarbox.core.business.R +import ch.srgssr.pillarbox.core.business.integrationlayer.data.BlockReason +import kotlin.test.Test +import kotlin.test.assertEquals + +class BlockReasonTest { + @Test + fun `get string resId for BlockReason`() { + assertEquals(R.string.blockReason_ageRating12, BlockReason.AGERATING12.getStringResId()) + assertEquals(R.string.blockReason_ageRating18, BlockReason.AGERATING18.getStringResId()) + assertEquals(R.string.blockReason_commercial, BlockReason.COMMERCIAL.getStringResId()) + assertEquals(R.string.blockReason_endDate, BlockReason.ENDDATE.getStringResId()) + assertEquals(R.string.blockReason_geoBlock, BlockReason.GEOBLOCK.getStringResId()) + assertEquals(R.string.blockReason_legal, BlockReason.LEGAL.getStringResId()) + assertEquals(R.string.blockReason_startDate, BlockReason.STARTDATE.getStringResId()) + assertEquals(R.string.blockReason_unknown, BlockReason.UNKNOWN.getStringResId()) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/MediaCompositionTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/MediaCompositionTest.kt new file mode 100644 index 000000000..ef3ec5e3e --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/MediaCompositionTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.integrationlayer.data + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class MediaCompositionTest { + @Test + fun `get main chapter`() { + val mediaComposition = MediaComposition( + chapterUrn = "urn:rts:video:8841634", + listChapter = listOf( + Chapter( + urn = "urn:rts:audio:3262363", + title = "Dvr", + imageUrl = "https://dvr.image/", + ), + Chapter( + urn = "urn:rts:video:8841634", + title = "Live", + imageUrl = "https://live.image/", + ), + Chapter( + urn = "urn:rts:video:13444428", + title = "Short", + imageUrl = "https://short.image/", + ), + ), + ) + val mainChapter = mediaComposition.mainChapter + + assertEquals(mediaComposition.listChapter[1], mainChapter) + } + + @Test(expected = NullPointerException::class) + fun `get main chapter with empty chapter list`() { + val mediaComposition = MediaComposition( + chapterUrn = "urn:rts:video:8841634", + listChapter = emptyList(), + ) + mediaComposition.mainChapter + } + + @Test + fun `find chapter by urn`() { + val mediaComposition = MediaComposition( + chapterUrn = "urn:rts:video:8841634", + listChapter = listOf( + Chapter( + urn = "urn:rts:audio:3262363", + title = "Dvr", + imageUrl = "https://dvr.image/", + ), + Chapter( + urn = "urn:rts:video:8841634", + title = "Live", + imageUrl = "https://live.image/", + ), + Chapter( + urn = "urn:rts:video:13444428", + title = "Short", + imageUrl = "https://short.image/", + ), + ), + ) + val chapter = mediaComposition.findChapterByUrn(mediaComposition.chapterUrn) + + assertEquals(mediaComposition.listChapter[1], chapter) + } + + @Test + fun `find chapter by urn with empty chapter list`() { + val mediaComposition = MediaComposition( + chapterUrn = "urn:rts:video:8841634", + listChapter = emptyList(), + ) + val chapter = mediaComposition.findChapterByUrn(mediaComposition.chapterUrn) + + assertNull(chapter) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/MediaUrnTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/MediaUrnTest.kt new file mode 100644 index 000000000..40b082acf --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/MediaUrnTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.integrationlayer.data + +import androidx.media3.common.MediaItem.ClippingConfiguration +import androidx.media3.common.MediaItem.LiveConfiguration +import androidx.media3.common.MediaItem.RequestMetadata +import androidx.media3.common.MediaMetadata +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +class MediaUrnTest { + @Test + fun `is valid media urn`() { + urnData.forEach { (urn, isValid) -> + assertEquals(isValid, urn.isValidMediaUrn(), "Expected '$urn' to be ${if (isValid) "a valid" else "an invalid"} urn") + } + } + + @Test + fun `is valid media urn, urn is null`() { + val urn: String? = null + + assertFalse(urn.isValidMediaUrn()) + } + + @Test + fun `MediaUrn is valid`() { + urnData.forEach { (urn, isValid) -> + assertEquals(isValid, MediaUrn.isValid(urn), "Expected '$urn' to be ${if (isValid) "a valid" else "an invalid"} urn") + } + } + + @Test + fun `create media item`() { + val urn = "urn:rts:video:123345" + val mediaItem = MediaUrn.createMediaItem(urn) + + assertEquals(urn, mediaItem.mediaId) + assertEquals(ClippingConfiguration.Builder().build(), mediaItem.clippingConfiguration) + assertNull(mediaItem.localConfiguration) + assertEquals(LiveConfiguration.Builder().build(), mediaItem.liveConfiguration) + assertEquals(MediaMetadata.EMPTY, mediaItem.mediaMetadata) + assertEquals(RequestMetadata.EMPTY, mediaItem.requestMetadata) + } + + @Test(expected = IllegalArgumentException::class) + fun `create media item, invalid urn`() { + MediaUrn.createMediaItem("urn:rts:channel:tv:1234") + } + + private companion object { + private val urnData = mapOf( + "" to false, + " " to false, + "Hello guys!" to false, + "https://www.rts.ch/media" to false, + "https://www.rts.ch/media/urn:rts:video:123345" to false, + "urn:rts:channel:tv:1234" to false, + "urn:rts:show:tv:1234" to false, + "urn:rts:audio:123345" to true, + "urn:rts:video:123345" to true, + ) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultMediaCompositionDataSourceTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultMediaCompositionDataSourceTest.kt new file mode 100644 index 000000000..80f6c47f9 --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/integrationlayer/service/DefaultMediaCompositionDataSourceTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.integrationlayer.service + +import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.engine.mock.respondBadRequest +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class DefaultMediaCompositionDataSourceTest { + @Test + fun `get media composition by urn`() = runTest { + val vector = Vector.MOBILE + val mockEngine = MockEngine { + respond( + content = """ + { + "chapterUrn": "$URN", + "chapterList": [] + } + """.trimIndent(), + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + val mediaCompositionDataSource = DefaultMediaCompositionDataSource( + httpClient = createHttpClient(mockEngine), + baseUrl = ilHost, + vector = vector, + ) + + val mediaCompositionResult = mediaCompositionDataSource.getMediaCompositionByUrn(URN) + val requestUrl = mockEngine.requestHistory.singleOrNull()?.url?.toString() + + assertTrue(mediaCompositionResult.isSuccess) + assertEquals("${ilHost}integrationlayer/2.1/mediaComposition/byUrn/$URN?vector=$vector&onlyChapters=true", requestUrl) + + val mediaComposition = mediaCompositionResult.getOrThrow() + assertEquals(URN, mediaComposition.chapterUrn) + assertTrue(mediaComposition.listChapter.isEmpty()) + } + + @Test + fun `get media composition by urn, when request fails`() = runTest { + val vector = Vector.TV + val mockEngine = MockEngine { + respondBadRequest() + } + val mediaCompositionDataSource = DefaultMediaCompositionDataSource( + httpClient = createHttpClient(mockEngine), + baseUrl = ilHost, + vector = vector, + ) + + val mediaCompositionResult = mediaCompositionDataSource.getMediaCompositionByUrn(URN) + val requestUrl = mockEngine.requestHistory.singleOrNull()?.url?.toString() + + assertTrue(mediaCompositionResult.isFailure) + assertEquals("${ilHost}integrationlayer/2.1/mediaComposition/byUrn/$URN?vector=$vector&onlyChapters=true", requestUrl) + } + + private companion object { + private val ilHost = IlHost.TEST + private const val URN = "urn:rts:video:123345" + + private fun createHttpClient(engine: HttpClientEngine): HttpClient { + return HttpClient(engine) { + expectSuccess = true + + install(ContentNegotiation) { + json(DefaultHttpClient.jsonSerializer) + } + } + } + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt new file mode 100644 index 000000000..9d7574613 --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/DefaultMediaItemTrackerRepositoryTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.tracker + +import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct +import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTracker +import ch.srgssr.pillarbox.core.business.tracker.comscore.ComScoreTracker +import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository +import io.mockk.mockk +import io.mockk.verifySequence +import kotlin.test.Test + +class DefaultMediaItemTrackerRepositoryTest { + @Test + fun `DefaultMediaItemTrackerRepository registers some default factories`() { + val trackerRepository = mockk(relaxed = true) + val commandersAct = mockk() + + DefaultMediaItemTrackerRepository( + trackerRepository = trackerRepository, + commandersAct = commandersAct, + ) + + verifySequence { + trackerRepository.registerFactory(SRGEventLoggerTracker::class.java, any(SRGEventLoggerTracker.Factory::class)) + trackerRepository.registerFactory(ComScoreTracker::class.java, any(ComScoreTracker.Factory::class)) + trackerRepository.registerFactory(CommandersActTracker::class.java, any(CommandersActTracker.Factory::class)) + } + } + @Test + fun `DefaultMediaItemTrackerRepository registers some default factories without CommandersAct`() { + val trackerRepository = mockk(relaxed = true) + + DefaultMediaItemTrackerRepository( + trackerRepository = trackerRepository, + commandersAct = null, + ) + + verifySequence { + trackerRepository.registerFactory(SRGEventLoggerTracker::class.java, any(SRGEventLoggerTracker.Factory::class)) + trackerRepository.registerFactory(ComScoreTracker::class.java, any(ComScoreTracker.Factory::class)) + trackerRepository.registerFactory(CommandersActTracker::class.java, any(CommandersActTracker.Factory::class)) + } + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTrackerTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTrackerTest.kt new file mode 100644 index 000000000..a86ebce89 --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/SRGEventLoggerTrackerTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.tracker + +import androidx.media3.exoplayer.ExoPlayer +import ch.srgssr.pillarbox.player.tracker.MediaItemTracker +import io.mockk.mockk +import io.mockk.verifySequence +import kotlin.test.Test + +class SRGEventLoggerTrackerTest { + @Test + fun `event logger`() { + val player = mockk(relaxed = true) + val eventLogger = SRGEventLoggerTracker.Factory().create() + + eventLogger.start(player, initialData = null) + eventLogger.update(data = "") + eventLogger.stop(player, MediaItemTracker.StopReason.EoF, positionMs = 0L) + + verifySequence { + player.addAnalyticsListener(any()) + player.removeAnalyticsListener(any()) + } + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/TotalPlaytimeCounterTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/TotalPlaytimeCounterTest.kt new file mode 100644 index 000000000..5890bb1bc --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/TotalPlaytimeCounterTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.tracker + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class TotalPlaytimeCounterTest { + @Test + @OptIn(ExperimentalCoroutinesApi::class) + fun `total playtime counter`() = runTest { + advanceTimeBy(10.seconds) + + val counter = TotalPlaytimeCounter { currentTime } + assertEquals(0.milliseconds, counter.getTotalPlayTime()) + + counter.play() + advanceTimeBy(5.seconds) + assertEquals(5.seconds, counter.getTotalPlayTime()) + + counter.pause() + advanceTimeBy(5.seconds) + assertEquals(5.seconds, counter.getTotalPlayTime()) + + counter.play() + advanceTimeBy(5.seconds) + assertEquals(10.seconds, counter.getTotalPlayTime()) + + counter.pause() + advanceTimeBy(5.seconds) + assertEquals(10.seconds, counter.getTotalPlayTime()) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt new file mode 100644 index 000000000..c326a19bc --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreamingTest.kt @@ -0,0 +1,242 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.tracker.commandersact + +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import androidx.media3.common.C +import androidx.media3.common.DeviceInfo +import androidx.media3.common.Format +import androidx.media3.common.MimeTypes +import androidx.media3.common.TrackGroup +import androidx.media3.common.Tracks +import androidx.media3.exoplayer.ExoPlayer +import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct +import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType +import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent +import io.mockk.Called +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class CommandersActStreamingTest { + @Test + fun `commanders act streaming, player not playing initially`() { + val commandersAct = mockk(relaxed = true) + + val commandersActStreaming = CommandersActStreaming( + commandersAct = commandersAct, + player = createExoPlayer(isPlaying = false), + currentData = CommandersActTracker.Data(assets = emptyMap()), + ) + + verify { + commandersAct wasNot Called + } + + commandersActStreaming.notifyStop( + position = 30.seconds, + isEoF = false, + ) + + verify { + commandersAct wasNot Called + } + } + + @Test + fun `commanders act streaming, player playing initially, live`() { + val tcMediaEventSlot = slot() + val commandersAct = mockk { + justRun { sendTcMediaEvent(capture(tcMediaEventSlot)) } + } + val commandersActStreaming = CommandersActStreaming( + commandersAct = commandersAct, + player = createExoPlayer( + isPlaying = true, + currentPosition = 2.seconds.inWholeMilliseconds, + isCurrentMediaItemLive = true, + volume = 0.5f, + deviceVolume = 25, + duration = 45.seconds.inWholeMilliseconds, + ), + currentData = CommandersActTracker.Data( + assets = mapOf( + "key1" to "value1", + ), + sourceId = "source_id", + ), + ) + + verify { + commandersAct.sendTcMediaEvent(any()) + } + + assertTrue(tcMediaEventSlot.isCaptured) + + val tcMediaEventPlay = tcMediaEventSlot.captured + assertEquals(MediaEventType.Play, tcMediaEventPlay.eventType) + assertEquals(commandersActStreaming.currentData.assets, tcMediaEventPlay.assets) + assertEquals(commandersActStreaming.currentData.sourceId, tcMediaEventPlay.sourceId) + assertFalse(tcMediaEventPlay.isSubtitlesOn) + assertNull(tcMediaEventPlay.subtitleSelectionLanguage) + assertEquals(C.LANGUAGE_UNDETERMINED, tcMediaEventPlay.audioTrackLanguage) + assertEquals(43.seconds, tcMediaEventPlay.timeShift) + assertEquals(0.25f, tcMediaEventPlay.deviceVolume) + assertEquals(0.milliseconds, tcMediaEventPlay.mediaPosition) + + commandersActStreaming.notifyStop( + position = 30.seconds, + isEoF = true, + ) + + val tcMediaEventStop = tcMediaEventSlot.captured + assertEquals(MediaEventType.Eof, tcMediaEventStop.eventType) + assertEquals(commandersActStreaming.currentData.assets, tcMediaEventStop.assets) + assertEquals(commandersActStreaming.currentData.sourceId, tcMediaEventStop.sourceId) + assertFalse(tcMediaEventStop.isSubtitlesOn) + assertNull(tcMediaEventStop.subtitleSelectionLanguage) + assertEquals(C.LANGUAGE_UNDETERMINED, tcMediaEventStop.audioTrackLanguage) + assertEquals(15.seconds, tcMediaEventStop.timeShift) + assertEquals(0.25f, tcMediaEventStop.deviceVolume) + } + + @Test + fun `commanders act streaming, player playing initially, not live`() = runTest { + val tcMediaEventSlot = slot() + val commandersAct = mockk { + justRun { sendTcMediaEvent(capture(tcMediaEventSlot)) } + } + val commandersActStreaming = CommandersActStreaming( + commandersAct = commandersAct, + player = createExoPlayer( + isPlaying = true, + duration = 45.seconds.inWholeMilliseconds, + currentTracks = Tracks( + listOf( + createTracks( + label = "Text", + language = "fr", + sampleMimeType = MimeTypes.APPLICATION_TTML, + ), + createTracks( + label = "Audio", + language = "en", + sampleMimeType = MimeTypes.AUDIO_MP4, + ), + ), + ), + ), + currentData = CommandersActTracker.Data( + assets = mapOf( + "key1" to "value1", + ), + sourceId = "source_id", + ), + ) + + verify { + commandersAct.sendTcMediaEvent(any()) + } + + assertTrue(tcMediaEventSlot.isCaptured) + + val tcMediaEvent = tcMediaEventSlot.captured + assertEquals(MediaEventType.Play, tcMediaEvent.eventType) + assertEquals(commandersActStreaming.currentData.assets, tcMediaEvent.assets) + assertEquals(commandersActStreaming.currentData.sourceId, tcMediaEvent.sourceId) + assertTrue(tcMediaEvent.isSubtitlesOn) + assertEquals("fr", tcMediaEvent.subtitleSelectionLanguage) + assertEquals("en", tcMediaEvent.audioTrackLanguage) + assertNull(tcMediaEvent.timeShift) + assertEquals(0f, tcMediaEvent.deviceVolume) + assertEquals(0.milliseconds, tcMediaEvent.mediaPosition) + + commandersActStreaming.notifyStop( + position = 30.seconds, + isEoF = false, + ) + + val tcMediaEventStop = tcMediaEventSlot.captured + assertEquals(MediaEventType.Stop, tcMediaEventStop.eventType) + assertEquals(commandersActStreaming.currentData.assets, tcMediaEventStop.assets) + assertEquals(commandersActStreaming.currentData.sourceId, tcMediaEventStop.sourceId) + assertTrue(tcMediaEvent.isSubtitlesOn) + assertEquals("fr", tcMediaEvent.subtitleSelectionLanguage) + assertEquals("en", tcMediaEvent.audioTrackLanguage) + assertNull(tcMediaEvent.timeShift) + assertEquals(0f, tcMediaEventStop.deviceVolume) + assertEquals(30.seconds, tcMediaEventStop.mediaPosition) + } + + private fun createExoPlayer( + isPlaying: Boolean, + currentPosition: Long = 0L, + isCurrentMediaItemLive: Boolean = false, + deviceInfo: DeviceInfo = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL) + .setMinVolume(0) + .setMaxVolume(100) + .build(), + @FloatRange(from = 0.0, to = 1.0) volume: Float = 0f, + @IntRange(from = 0) deviceVolume: Int = 0, + duration: Long = 0L, + currentTracks: Tracks = Tracks.EMPTY, // groups, audio + ): ExoPlayer { + return mockk { + val player = this + + every { player.isPlaying } returns isPlaying + every { player.currentPosition } returns currentPosition + every { player.isCurrentMediaItemLive } returns isCurrentMediaItemLive + every { player.deviceInfo } returns deviceInfo + every { player.volume } returns volume + every { player.deviceVolume } returns deviceVolume + every { player.duration } returns duration + every { player.currentTracks } returns currentTracks + } + } + + private fun createTracks( + label: String, + language: String, + sampleMimeType: String, + ): Tracks.Group { + val mediaTrackGroup = listOf( + Format.Builder() + .setId("${label}_1") + .setLabel("$label 1") + .setLanguage(language) + .setSampleMimeType(sampleMimeType) + .build(), + Format.Builder() + .setId("${label}_2") + .setLabel("$label 2") + .setLanguage(language) + .setSampleMimeType(sampleMimeType) + .setSelectionFlags(C.SELECTION_FLAG_FORCED) + .build(), + ) + + return Tracks.Group( + TrackGroup(*mediaTrackGroup.toTypedArray()), + false, + IntArray(mediaTrackGroup.size) { C.FORMAT_HANDLED }, + BooleanArray(mediaTrackGroup.size) { + // select the second track + it == 1 + }, + ) + } +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt new file mode 100644 index 000000000..74d4aa8be --- /dev/null +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.tracker.commandersact + +import androidx.media3.common.C +import androidx.media3.exoplayer.ExoPlayer +import ch.srgssr.pillarbox.analytics.commandersact.CommandersAct +import ch.srgssr.pillarbox.analytics.commandersact.MediaEventType +import ch.srgssr.pillarbox.analytics.commandersact.TCMediaEvent +import ch.srgssr.pillarbox.player.tracker.MediaItemTracker +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class CommandersActTrackerTest { + @Test(expected = IllegalArgumentException::class) + fun `start() requires a non-null initial data`() { + val player = mockk(relaxed = true) + val commandersActs = mockk(relaxed = true) + val commandersActTracker = CommandersActTracker(commandersActs) + + commandersActTracker.start( + player = player, + initialData = null, + ) + } + + @Test(expected = IllegalArgumentException::class) + fun `start() requires an instance of CommandersActTracker#Data instance for the initial data`() { + val player = mockk(relaxed = true) + val commandersActs = mockk(relaxed = true) + val commandersActTracker = CommandersActTracker(commandersActs) + + commandersActTracker.start( + player = player, + initialData = "My data", + ) + } + + @Test(expected = IllegalArgumentException::class) + fun `update() requires an instance of CommandersActTracker#Data instance for the data`() { + val commandersActs = mockk(relaxed = true) + val commandersActTracker = CommandersActTracker(commandersActs) + + commandersActTracker.update(data = "My data") + } + + @Test + fun `commanders act tracker`() { + val player = mockk(relaxed = true) { + every { isPlaying } returns true + } + val commandersAct = mockk(relaxed = true) + val commandersActTracker = CommandersActTracker(commandersAct) + val commandersActStreamingSlot = slot() + val tcMediaEventSlots = mutableListOf() + + commandersActTracker.start( + player = player, + initialData = CommandersActTracker.Data(emptyMap()), + ) + + verify { + commandersAct.enableRunningInBackground() + commandersAct.sendTcMediaEvent(any()) + + player.isPlaying + player.addAnalyticsListener(capture(commandersActStreamingSlot)) + } + + assertTrue(commandersActStreamingSlot.isCaptured) + + val commandersActStreaming = commandersActStreamingSlot.captured + val newData = CommandersActTracker.Data( + assets = mapOf( + "key1" to "value1", + ), + ) + + commandersActTracker.update( + data = newData, + ) + + assertEquals(newData, commandersActStreaming.currentData) + + commandersActTracker.stop( + player = player, + reason = MediaItemTracker.StopReason.EoF, + positionMs = 30.seconds.inWholeMilliseconds, + ) + + verify { + player.removeAnalyticsListener(commandersActStreaming) + commandersAct.sendTcMediaEvent(capture(tcMediaEventSlots)) + } + + val tcMediaEvent = tcMediaEventSlots.last() + assertEquals(MediaEventType.Eof, tcMediaEvent.eventType) + assertEquals(commandersActStreaming.currentData.assets, tcMediaEvent.assets) + assertEquals(commandersActStreaming.currentData.sourceId, tcMediaEvent.sourceId) + assertFalse(tcMediaEvent.isSubtitlesOn) + assertNull(tcMediaEvent.subtitleSelectionLanguage) + assertEquals(C.LANGUAGE_UNDETERMINED, tcMediaEvent.audioTrackLanguage) + assertNull(tcMediaEvent.timeShift) + assertEquals(0f, tcMediaEvent.deviceVolume) + assertEquals(30.seconds, tcMediaEvent.mediaPosition) + } +}