Skip to content

Commit 1e250fb

Browse files
committed
Add PlayLog test 13
1 parent b09bf4b commit 1e250fb

File tree

5 files changed

+265
-6
lines changed

5 files changed

+265
-6
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.tidal.sdk.player.playlog
2+
3+
import assertk.Assert
4+
import assertk.assertions.isCloseTo
5+
6+
internal fun Assert<Double>.isAssetPositionEqualTo(targetPosition: Double) =
7+
isCloseTo(targetPosition, 0.5)

player/src/androidTest/kotlin/com/tidal/sdk/player/playlog/SingleMediaProductPlayLogTest.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package com.tidal.sdk.player.playlog
22

33
import android.app.Application
44
import androidx.test.platform.app.InstrumentationRegistry
5-
import assertk.Assert
65
import assertk.assertThat
76
import assertk.assertions.isBetween
8-
import assertk.assertions.isCloseTo
97
import assertk.assertions.isEmpty
108
import assertk.assertions.isEqualTo
119
import com.google.gson.Gson
@@ -785,10 +783,6 @@ internal class SingleMediaProductPlayLogTest {
785783
eq(emptyMap()),
786784
)
787785
}
788-
789-
private fun Assert<Double>.isAssetPositionEqualTo(targetPosition: Double) = run {
790-
isCloseTo(targetPosition, 0.5)
791-
}
792786
}
793787

794788
private const val MEDIA_PRODUCT_DURATION_SECONDS = 5.055
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package com.tidal.sdk.player.playlog
2+
3+
import android.app.Application
4+
import androidx.test.platform.app.InstrumentationRegistry
5+
import assertk.assertThat
6+
import com.google.gson.Gson
7+
import com.google.gson.JsonObject
8+
import com.tidal.sdk.auth.CredentialsProvider
9+
import com.tidal.sdk.auth.model.AuthResult
10+
import com.tidal.sdk.auth.model.Credentials
11+
import com.tidal.sdk.auth.util.isLoggedIn
12+
import com.tidal.sdk.common.TidalMessage
13+
import com.tidal.sdk.eventproducer.EventSender
14+
import com.tidal.sdk.eventproducer.model.ConsentCategory
15+
import com.tidal.sdk.player.Player
16+
import com.tidal.sdk.player.common.model.MediaProduct
17+
import com.tidal.sdk.player.common.model.ProductType
18+
import com.tidal.sdk.player.events.EventReporterModuleRoot
19+
import com.tidal.sdk.player.events.di.DefaultEventReporterComponent
20+
import com.tidal.sdk.player.events.playlogtest.PlayLogTestDefaultEventReporterComponentFactory
21+
import com.tidal.sdk.player.events.reflectionComponentFactoryF
22+
import com.tidal.sdk.player.playbackengine.model.Event.MediaProductEnded
23+
import com.tidal.sdk.player.playbackengine.model.Event.Release
24+
import com.tidal.sdk.player.setBodyFromFile
25+
import kotlin.math.absoluteValue
26+
import kotlin.time.Duration.Companion.minutes
27+
import kotlinx.coroutines.Dispatchers
28+
import kotlinx.coroutines.flow.Flow
29+
import kotlinx.coroutines.flow.emptyFlow
30+
import kotlinx.coroutines.flow.filter
31+
import kotlinx.coroutines.flow.first
32+
import kotlinx.coroutines.launch
33+
import kotlinx.coroutines.runBlocking
34+
import kotlinx.coroutines.test.StandardTestDispatcher
35+
import kotlinx.coroutines.test.TestCoroutineScheduler
36+
import kotlinx.coroutines.test.TestScope
37+
import kotlinx.coroutines.test.advanceUntilIdle
38+
import kotlinx.coroutines.test.runTest
39+
import kotlinx.coroutines.withContext
40+
import kotlinx.coroutines.withTimeout
41+
import okhttp3.HttpUrl.Companion.toHttpUrl
42+
import okhttp3.OkHttpClient
43+
import okhttp3.mockwebserver.MockResponse
44+
import okhttp3.mockwebserver.MockWebServer
45+
import org.junit.After
46+
import org.junit.Before
47+
import org.junit.BeforeClass
48+
import org.junit.Rule
49+
import org.junit.Test
50+
import org.junit.runners.Parameterized
51+
import org.mockito.Mockito.atMost
52+
import org.mockito.Mockito.mock
53+
import org.mockito.Mockito.verifyNoMoreInteractions
54+
import org.mockito.kotlin.anyOrNull
55+
import org.mockito.kotlin.argThat
56+
import org.mockito.kotlin.eq
57+
import org.mockito.kotlin.verify
58+
59+
internal class TwoMediaProductsPlayLogTest {
60+
61+
@get:Rule
62+
val server = MockWebServer()
63+
64+
private val eventReporterCoroutineScope =
65+
TestScope(StandardTestDispatcher(TestCoroutineScheduler()))
66+
private val responseDispatcher = PlayLogTestMockWebServerDispatcher(server)
67+
private val eventSender = mock<EventSender>()
68+
private val mediaProduct1 = MediaProduct(ProductType.TRACK, "1", "TEST_1", "456")
69+
private val mediaProduct2 = MediaProduct(ProductType.TRACK, "2", "TEST_2", "789")
70+
private lateinit var player: Player
71+
72+
@Before
73+
fun setUp() {
74+
responseDispatcher[
75+
"https://api.tidal.com/v1/tracks/${mediaProduct1.productId}/playbackinfo?playbackmode=STREAM&assetpresentation=FULL&audioquality=LOW&immersiveaudio=true".toHttpUrl(),
76+
] = {
77+
MockResponse().setBodyFromFile(
78+
"api-responses/playbackinfo/tracks/playlogtest/get_1_bts.json",
79+
)
80+
}
81+
responseDispatcher["https://test.audio.tidal.com/1_bts.m4a".toHttpUrl()] = {
82+
MockResponse().setBodyFromFile("raw/playlogtest/1_bts.m4a")
83+
}
84+
responseDispatcher[
85+
"https://api.tidal.com/v1/tracks/${mediaProduct2.productId}/playbackinfo?playbackmode=STREAM&assetpresentation=FULL&audioquality=LOW&immersiveaudio=true".toHttpUrl(),
86+
] = {
87+
MockResponse().setBodyFromFile(
88+
"api-responses/playbackinfo/tracks/playlogtest/get_2_bts.json",
89+
)
90+
}
91+
responseDispatcher["https://test.audio.tidal.com/test_1min.m4a".toHttpUrl()] = {
92+
MockResponse().setBodyFromFile("raw/playlogtest/test_1min.m4a")
93+
}
94+
EventReporterModuleRoot.reflectionComponentFactoryF = {
95+
PlayLogTestDefaultEventReporterComponentFactory(eventReporterCoroutineScope)
96+
}
97+
server.dispatcher = responseDispatcher
98+
99+
player = Player(
100+
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
101+
as Application,
102+
object : CredentialsProvider {
103+
private val CREDENTIALS = Credentials(
104+
clientId = "a client id",
105+
requestedScopes = emptySet(),
106+
clientUniqueKey = null,
107+
grantedScopes = emptySet(),
108+
userId = "a non-null user id",
109+
expires = null,
110+
token = "a non-null token",
111+
)
112+
113+
override val bus: Flow<TidalMessage> = emptyFlow()
114+
115+
override suspend fun getCredentials(apiErrorSubStatus: String?) =
116+
AuthResult.Success(CREDENTIALS)
117+
118+
override fun isUserLoggedIn() = CREDENTIALS.isLoggedIn()
119+
},
120+
eventSender = eventSender,
121+
okHttpClient = OkHttpClient.Builder()
122+
.addInterceptor {
123+
val request = it.request()
124+
val mockWebServerUrl = responseDispatcher.urlRecords[request.url]
125+
?: return@addInterceptor it.proceed(request)
126+
it.proceed(
127+
request.newBuilder()
128+
.url(mockWebServerUrl)
129+
.build(),
130+
)
131+
}.build(),
132+
)
133+
}
134+
135+
companion object {
136+
private lateinit var originalEventReporterComponentFactoryF:
137+
() -> DefaultEventReporterComponent.Factory
138+
private const val MEDIA_PRODUCT_1_DURATION_SECONDS = 5.055
139+
private const val MEDIA_PRODUCT_2_DURATION_SECONDS = 60.606667
140+
141+
@BeforeClass
142+
@JvmStatic
143+
fun beforeAll() {
144+
originalEventReporterComponentFactoryF =
145+
EventReporterModuleRoot.reflectionComponentFactoryF
146+
}
147+
148+
@JvmStatic
149+
@Parameterized.Parameters
150+
fun parameters(): List<Array<MediaProduct>> {
151+
val mediaProduct1s = setOf(
152+
MediaProduct(ProductType.TRACK, "1", "TESTA", "456"),
153+
MediaProduct(ProductType.TRACK, "1", null, "789"),
154+
MediaProduct(ProductType.TRACK, "1", "TESTB", null),
155+
MediaProduct(ProductType.TRACK, "1", null, null),
156+
)
157+
val mediaProduct2s = setOf(
158+
MediaProduct(ProductType.TRACK, "2", "TESTA", "456"),
159+
MediaProduct(ProductType.TRACK, "2", null, "789"),
160+
MediaProduct(ProductType.TRACK, "2", "TESTB", null),
161+
MediaProduct(ProductType.TRACK, "2", null, null),
162+
)
163+
return mediaProduct1s.flatMap { mediaProduct1 ->
164+
mediaProduct2s.map { mediaProduct2 ->
165+
arrayOf(mediaProduct1, mediaProduct2)
166+
}
167+
}
168+
}
169+
}
170+
171+
@After
172+
fun afterEach() {
173+
runBlocking {
174+
val job = launch { player.playbackEngine.events.first { it is Release } }
175+
player.release()
176+
job.join()
177+
}
178+
verify(eventSender, atMost(Int.MAX_VALUE))
179+
.sendEvent(
180+
argThat { !contentEquals("playback_session") },
181+
anyOrNull(),
182+
anyOrNull(),
183+
anyOrNull(),
184+
)
185+
verifyNoMoreInteractions(eventSender)
186+
}
187+
188+
@Test
189+
fun playSequentially() = runTest(timeout = 3.minutes) {
190+
val gson = Gson()
191+
192+
player.playbackEngine.load(mediaProduct1)
193+
player.playbackEngine.setNext(mediaProduct2)
194+
player.playbackEngine.play()
195+
withContext(Dispatchers.Default.limitedParallelism(1)) {
196+
withTimeout(2.minutes) {
197+
player.playbackEngine.events.filter { it is MediaProductEnded }.first()
198+
}
199+
}
200+
201+
eventReporterCoroutineScope.advanceUntilIdle()
202+
verify(eventSender).sendEvent(
203+
eq("playback_session"),
204+
eq(ConsentCategory.NECESSARY),
205+
argThat {
206+
with(gson.fromJson(this, JsonObject::class.java)["payload"].asJsonObject) {
207+
// https://github.com/androidx/media/issues/1252
208+
get("startAssetPosition").asDouble.isAssetPositionEqualTo(0.0) &&
209+
// https://github.com/androidx/media/issues/1253
210+
get("endAssetPosition").asDouble
211+
.isAssetPositionEqualTo(MEDIA_PRODUCT_1_DURATION_SECONDS) &&
212+
get("actualProductId")?.asString.contentEquals(mediaProduct1.productId) &&
213+
get("sourceType")?.asString.contentEquals(mediaProduct1.sourceType) &&
214+
get("sourceId")?.asString.contentEquals(mediaProduct1.sourceId) &&
215+
get("actions").asJsonArray.isEmpty
216+
}
217+
},
218+
eq(emptyMap()),
219+
)
220+
verify(eventSender).sendEvent(
221+
eq("playback_session"),
222+
eq(ConsentCategory.NECESSARY),
223+
argThat {
224+
with(gson.fromJson(this, JsonObject::class.java)["payload"].asJsonObject) {
225+
// https://github.com/androidx/media/issues/1252
226+
assertThat(get("startAssetPosition").asDouble).isAssetPositionEqualTo(0.0)
227+
// https://github.com/androidx/media/issues/1253
228+
get("endAssetPosition").asDouble
229+
.isAssetPositionEqualTo(MEDIA_PRODUCT_2_DURATION_SECONDS) &&
230+
get("actualProductId")?.asString.contentEquals(mediaProduct2.productId) &&
231+
get("sourceType")?.asString.contentEquals(mediaProduct2.sourceType) &&
232+
get("sourceId")?.asString.contentEquals(mediaProduct2.sourceId) &&
233+
get("actions").asJsonArray.isEmpty
234+
}
235+
},
236+
eq(emptyMap()),
237+
)
238+
}
239+
240+
private fun Double.isAssetPositionEqualTo(targetPosition: Double) =
241+
(this - targetPosition).absoluteValue < 0.5
242+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"trackId": 2,
3+
"assetPresentation": "FULL",
4+
"audioMode": "STEREO",
5+
"audioQuality": "LOW",
6+
"streamingSessionId": "356",
7+
"manifestMimeType": "application/vnd.tidal.bts",
8+
"manifestHash": "a manifest hash",
9+
"manifest": "eyJtaW1lVHlwZSI6ImF1ZGlvL21wNCIsImNvZGVjcyI6Im1wNGEuNDAuNSIsInVybHMiOlsiaHR0cHM6Ly90ZXN0LmF1ZGlvLnRpZGFsLmNvbS90ZXN0XzFtaW4ubTRhIl19",
10+
"albumReplayGain": -9.8,
11+
"albumPeakAmplitude": 0.999923,
12+
"trackReplayGain": -9.8,
13+
"trackPeakAmplitude": 0.999923,
14+
"bitDepth": 16,
15+
"sampleRate": 44100
16+
}
1.79 MB
Binary file not shown.

0 commit comments

Comments
 (0)