diff --git a/player/events/src/main/java/com/tidal/sdk/player/events/model/PlaybackSession.kt b/player/events/src/main/java/com/tidal/sdk/player/events/model/PlaybackSession.kt index e84ea4ab..e9502b65 100644 --- a/player/events/src/main/java/com/tidal/sdk/player/events/model/PlaybackSession.kt +++ b/player/events/src/main/java/com/tidal/sdk/player/events/model/PlaybackSession.kt @@ -34,10 +34,10 @@ sealed class PlaybackSession : @Keep @SuppressWarnings("UnusedPrivateMember") data class Action( - private val timestamp: Long, + val timestamp: Long, @SerializedName("assetPosition") - private val assetPositionSeconds: Double, - private val actionType: Type, + val assetPositionSeconds: Double, + val actionType: Type, ) { @Keep diff --git a/player/src/androidTest/kotlin/com/tidal/sdk/player/playlog/PlayLogTest.kt b/player/src/androidTest/kotlin/com/tidal/sdk/player/playlog/PlayLogTest.kt index 90c2b897..2812f953 100644 --- a/player/src/androidTest/kotlin/com/tidal/sdk/player/playlog/PlayLogTest.kt +++ b/player/src/androidTest/kotlin/com/tidal/sdk/player/playlog/PlayLogTest.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.test.platform.app.InstrumentationRegistry import assertk.Assert import assertk.assertThat +import assertk.assertions.isBetween import assertk.assertions.isCloseTo import assertk.assertions.isEqualTo import com.google.gson.Gson @@ -20,6 +21,7 @@ import com.tidal.sdk.player.common.model.MediaProduct import com.tidal.sdk.player.common.model.ProductType import com.tidal.sdk.player.events.EventReporterModuleRoot import com.tidal.sdk.player.events.di.DefaultEventReporterComponent +import com.tidal.sdk.player.events.model.PlaybackSession import com.tidal.sdk.player.events.playlogtest.PlayLogTestDefaultEventReporterComponentFactory import com.tidal.sdk.player.events.reflectionComponentFactoryF import com.tidal.sdk.player.playbackengine.model.Event @@ -27,7 +29,10 @@ import com.tidal.sdk.player.playbackengine.model.Event.MediaProductEnded import com.tidal.sdk.player.repeatableflakytest.RepeatableFlakyTest import com.tidal.sdk.player.repeatableflakytest.RepeatableFlakyTestRule import com.tidal.sdk.player.setBodyFromFile +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter @@ -137,7 +142,7 @@ class PlayLogTest { argThat { !contentEquals("playback_session") }, anyOrNull(), anyOrNull(), - anyOrNull() + anyOrNull(), ) verifyNoMoreInteractions(eventSender) } @@ -163,7 +168,7 @@ class PlayLogTest { loadAndPlayUntilEnd(MediaProduct(ProductType.TRACK, "1", null, null)) private fun loadAndPlayUntilEnd(mediaProduct: MediaProduct) = runTest { - responseDispatcher [ + responseDispatcher[ "https://api.tidal.com/v1/tracks/1/playbackinfo?playbackmode=STREAM&assetpresentation=FULL&audioquality=LOW".toHttpUrl(), ] = { MockResponse().setBodyFromFile( @@ -201,6 +206,87 @@ class PlayLogTest { ) } + @Test + fun loadAndPlayThenPauseThenPlayNoNulls() = + loadAndPlayThenPauseThenPlay(MediaProduct(ProductType.TRACK, "1", "TESTA", "456")) + + @Test + fun loadAndPlayThenPauseThenPlayNullSourceType() = + loadAndPlayThenPauseThenPlay(MediaProduct(ProductType.TRACK, "1", null, "789")) + + @Test + fun loadAndPlayThenPauseThenPlayNullSourceId() = + loadAndPlayThenPauseThenPlay(MediaProduct(ProductType.TRACK, "1", "TESTB", null)) + + @Test + fun loadAndPlayThenPauseThenPlayNullSourceTypeNullSourceId() = + loadAndPlayThenPauseThenPlay(MediaProduct(ProductType.TRACK, "1", null, null)) + + @Suppress("LongMethod") + private fun loadAndPlayThenPauseThenPlay(mediaProduct: MediaProduct) = runTest { + val gson = Gson() + responseDispatcher[ + "https://api.tidal.com/v1/tracks/1/playbackinfo?playbackmode=STREAM&assetpresentation=FULL&audioquality=LOW".toHttpUrl(), + ] = { + MockResponse().setBodyFromFile( + "api-responses/playbackinfo/tracks/playlogtest/get_1_bts.json", + ) + } + responseDispatcher["https://test.audio.tidal.com/1_bts.m4a".toHttpUrl()] = { + MockResponse().setBodyFromFile("raw/playlogtest/1_bts.m4a") + } + + player.playbackEngine.load(mediaProduct) + player.playbackEngine.play() + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(4.seconds) { + player.playbackEngine.events.filter { it is Event.MediaProductTransition }.first() + } + delay(2.seconds) + while (player.playbackEngine.assetPosition < 2) { + delay(10.milliseconds) + } + player.playbackEngine.pause() + delay(1.seconds) + player.playbackEngine.play() + withTimeout(8000) { + player.playbackEngine.events.filter { it is MediaProductEnded }.first() + } + } + + eventReporterCoroutineScope.advanceUntilIdle() + verify(eventSender).sendEvent( + eq("playback_session"), + eq(ConsentCategory.NECESSARY), + argThat { + with(gson.fromJson(this, JsonObject::class.java)["payload"].asJsonObject) { + assertThat(get("startAssetPosition").asDouble).isAssetPositionEqualTo(0.0) + assertThat(get("endAssetPosition").asDouble).isAssetPositionEqualTo(5.065) + assertThat(get("actualProductId").asString).isEqualTo(mediaProduct.productId) + assertThat(get("sourceType")?.asString).isEqualTo(mediaProduct.sourceType) + assertThat(get("sourceId")?.asString).isEqualTo(mediaProduct.sourceId) + with(get("actions").asJsonArray) { + val pauseAction = + gson.fromJson(this[0], PlaybackSession.Payload.Action::class.java) + assertThat(pauseAction.actionType) + .isEqualTo(PlaybackSession.Payload.Action.Type.PLAYBACK_STOP) + val resumeAction = + gson.fromJson(this[1], PlaybackSession.Payload.Action::class.java) + assertThat(resumeAction.actionType) + .isEqualTo(PlaybackSession.Payload.Action.Type.PLAYBACK_START) + assertThat(resumeAction.assetPositionSeconds) + .isAssetPositionEqualTo(pauseAction.assetPositionSeconds) + val perfectResumeTimestamp = pauseAction.timestamp + 1_000 + assertThat(resumeAction.timestamp) + .isBetween(perfectResumeTimestamp - 500, perfectResumeTimestamp + 500) + } + } + true + }, + eq(emptyMap()), + ) + } + private fun Assert.isAssetPositionEqualTo(targetPosition: Double) = run { isCloseTo(targetPosition, 0.5) }