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

Provide tools to test a player #388

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ androidx-media = "1.7.0"
androidx-media3 = "1.2.0"
androidx-navigation = "2.7.6"
androidx-paging = "3.2.1"
androidx-test = "1.5.0"
androidx-test-espresso = "3.5.1"
androidx-test-ext-junit = "1.1.5"
androidx-test-monitor = "1.6.1"
androidx-test-runner = "1.5.2"
Expand Down Expand Up @@ -86,6 +88,7 @@ androidx-media3-ui-leanback = { group = "androidx.media3", name = "media3-ui-lea
androidx-media3-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidx-media3" }
androidx-media3-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "androidx-media3" }
androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "androidx-media3" }
androidx-media3-test-utils = { module = "androidx.media3:media3-test-utils", version.ref = "androidx-media3" }
androidx-media = { group = "androidx.media", name = "media", version.ref = "androidx-media" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
Expand All @@ -112,6 +115,9 @@ androidx-compose-material-icons-extended = { module = "androidx.compose.material
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" }
leanback = { group = "androidx.leanback", name = "leanback", version.ref = "androidx-leanback" }
androidx-test = { module = "androidx.test:core", version.ref = "androidx-test" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" }
androidx-test-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "androidx-test-espresso" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }

Expand Down
10 changes: 10 additions & 0 deletions pillarbox-ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ dependencies {
implementation(libs.kotlinx.coroutines.core)

debugImplementation(libs.androidx.compose.ui.tooling)

androidTestImplementation(libs.androidx.media3.exoplayer)
androidTestImplementation(libs.androidx.media3.test.utils)
androidTestImplementation(libs.androidx.test)
androidTestImplementation(libs.androidx.test.espresso.core)
androidTestImplementation(libs.androidx.test.espresso.idling.resource)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestRuntimeOnly(libs.androidx.test.runner)
androidTestImplementation(libs.junit)
androidTestImplementation(libs.kotlinx.coroutines.test)
}

publishing {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.ui

import android.os.Looper
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Player.Listener
import androidx.media3.test.utils.FakeClock
import androidx.media3.test.utils.TestExoPlayerBuilder
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExternalResource
import org.junit.runner.RunWith
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

// TODO Move this in "pillarbox-player-testutils"
private class PlayerIdlingResource(
@Player.State private val expectedPlaybackState: Int
) : IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null

@Player.State
var playbackState: Int = Player.STATE_IDLE
set(value) {
field = value

if (playbackState == expectedPlaybackState) {
callback?.onTransitionToIdle()
}
}

override fun getName(): String {
return "PlayerIdlingResource"
}

override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}

override fun isIdleNow(): Boolean {
return playbackState == expectedPlaybackState
}
}

// TODO Move this in "pillarbox-player-testutils"
class ExoPlayerRule(
private val mediaUri: String,
@Player.State private val waitForPlaybackState: Int
) : ExternalResource() {
private val playerIdlingResource = PlayerIdlingResource(waitForPlaybackState)

lateinit var clock: FakeClock
private set

lateinit var player: Player
private set

override fun before() {
Looper.prepare()
IdlingRegistry.getInstance().register(playerIdlingResource)

setupClock()
setupPlayer()

Espresso.onIdle()
}

override fun after() {
player.release()

IdlingRegistry.getInstance().unregister(playerIdlingResource)
Looper.myLooper()?.quit()
}

private fun setupClock() {
clock = FakeClock(true)
}

private fun setupPlayer() {
player = TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setClock(clock)
.build()
player.addListener(
object : Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
playerIdlingResource.playbackState = playbackState
}
}
)
player.setMediaItem(MediaItem.fromUri(mediaUri))
player.prepare()
player.play()
}
}

@RunWith(AndroidJUnit4::class)
class TestSimpleProgressTrackerState {
@get:Rule
val playerRule = ExoPlayerRule(
mediaUri = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd",
waitForPlaybackState = Player.STATE_READY
)

@Test
fun progressWithoutManualChanges() = runTest {
val progressTrackerState = SimpleProgressTrackerState(playerRule.player, this)

val playerPositions = (0L..50L step 5L).toList()
playerPositions.forEach { playerPosition ->
playerRule.clock.advanceTime(playerPosition)
}

val actualProgress = mutableListOf<Duration>()
launch(coroutineContext) {
progressTrackerState.progress
.toList(actualProgress)
}.join()

assertEquals(playerPositions.map { it.milliseconds }, actualProgress)
}
}
Loading