diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7dedf56a1..4107ef060 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ okhttp = "4.12.0" srg-data-provider = "0.8.0" tag-commander-core = "5.4.2" tag-commander-server-side = "5.5.2" +turbine = "1.0.0" [libraries] accompanist-navigation-material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } @@ -120,6 +121,7 @@ androidx-compose-runtime-saveable = { module = "androidx.compose.runtime:runtime leanback = { group = "androidx.leanback", name = "leanback", version.ref = "androidx-leanback" } 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" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } [plugins] android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts index 6730d5159..f200a3ae2 100644 --- a/pillarbox-player/build.gradle.kts +++ b/pillarbox-player/build.gradle.kts @@ -83,6 +83,7 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.mockk) testImplementation(libs.mockk.dsl) + testImplementation(libs.turbine) androidTestImplementation(project(":pillarbox-player-testutils")) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt index 530e726c3..49bd84c79 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PlayerCallbackFlow.kt @@ -24,6 +24,9 @@ import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.transformLatest @@ -149,6 +152,7 @@ fun Player.mediaItemCountAsFlow(): Flow = callbackFlow { /** * Ticker emits event every [interval] when [Player.isPlaying] is true. + * Emit a value once at least once. */ @OptIn(ExperimentalCoroutinesApi::class) fun Player.tickerWhilePlayingAsFlow( @@ -162,9 +166,11 @@ fun Player.tickerWhilePlayingAsFlow( /** * Current position of the player update every [updateInterval] when it is playing. + * Send current position once if not playing. */ fun Player.currentPositionAsFlow(updateInterval: Duration = DefaultUpdateInterval): Flow = merge( + if (isPlaying) emptyFlow() else flowOf(currentPosition), tickerWhilePlayingAsFlow(updateInterval).map { currentPosition }, @@ -178,14 +184,11 @@ private fun Player.positionChangedFlow(): Flow = callbackFlow { newPosition: Player.PositionInfo, reason: Int ) { - if (reason == Player.DISCONTINUITY_REASON_SEEK) { - trySend(currentPosition) - } + trySend(newPosition.positionMs) } } - trySend(currentPosition) addPlayerListener(player = this@positionChangedFlow, listener) -} +}.distinctUntilChanged() /** * Current buffered percentage as flow [Player.getBufferedPercentage] diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestCurrentMediaItemTracker.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestCurrentMediaItemTracker.kt index 90793b8b6..1a9ccbb43 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestCurrentMediaItemTracker.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestCurrentMediaItemTracker.kt @@ -50,15 +50,15 @@ class TestCurrentMediaItemTracker { @Test fun testAreEqualsDifferentMediaItem() { - val mediaItem = createMediaItem("M1") - val mediaItem2 = createMediaItem("M2") + val mediaItem = createMediaItemWithMediaId("M1") + val mediaItem2 = createMediaItemWithMediaId("M2") Assert.assertFalse(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @Test fun testAreEqualsSameMediaId() { - val mediaItem = createMediaItem("M1") - val mediaItem2 = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") + val mediaItem2 = createMediaItemWithMediaId("M1") Assert.assertTrue(CurrentMediaItemTracker.areEqual(mediaItem, mediaItem2)) } @@ -95,7 +95,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartEnd() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.EOF) analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemEnd(mediaItem) @@ -105,7 +105,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartAsyncLoadEnd() = runTest { val mediaItemEmpty = MediaItem.Builder().setMediaId("M1").build() - val mediaItemLoaded = createMediaItem("M1") + val mediaItemLoaded = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.EOF) analyticsCommander.simulateItemStart(mediaItemEmpty) analyticsCommander.simulateItemLoaded(mediaItemLoaded) @@ -116,7 +116,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartAsyncLoadRelease() = runTest { val mediaItemEmpty = MediaItem.Builder().setMediaId("M1").build() - val mediaItemLoaded = createMediaItem("M1") + val mediaItemLoaded = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.END) analyticsCommander.simulateItemStart(mediaItemEmpty) analyticsCommander.simulateItemLoaded(mediaItemLoaded) @@ -126,7 +126,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartReleased() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.END) analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateRelease(mediaItem) @@ -135,7 +135,7 @@ class TestCurrentMediaItemTracker { @Test fun testRelease() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE) analyticsCommander.simulateRelease(mediaItem) Assert.assertEquals(expected, tracker.stateList) @@ -143,7 +143,7 @@ class TestCurrentMediaItemTracker { @Test fun testRestartAfterEnd() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.EOF, EventState.START, EventState.EOF) analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemEnd(mediaItem) @@ -157,15 +157,15 @@ class TestCurrentMediaItemTracker { @Test fun testMediaTransitionSeekToNext() = runTest { val expectedStates = listOf(EventState.IDLE, EventState.START, EventState.END, EventState.START, EventState.EOF) - val mediaItem = createMediaItem("M1") - val mediaItem2 = createMediaItem("M2") + val mediaItem = createMediaItemWithMediaId("M1") + val mediaItem2 = createMediaItemWithMediaId("M2") analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemTransitionSeek(mediaItem, mediaItem2) analyticsCommander.simulateItemEnd(mediaItem2) Assert.assertEquals("Different Item", expectedStates, tracker.stateList) tracker.clear() - val mediaItem3 = createMediaItem("M1") + val mediaItem3 = createMediaItemWithMediaId("M1") analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemTransitionSeek(mediaItem, mediaItem3) analyticsCommander.simulateItemEnd(mediaItem3) @@ -175,9 +175,9 @@ class TestCurrentMediaItemTracker { @Test fun testMediaItemTransitionWithAsyncItem() = runTest { val expectedStates = listOf(EventState.IDLE, EventState.START, EventState.END, EventState.START, EventState.END) - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val mediaItem2 = MediaItem.Builder().setMediaId("M2").build() - val mediaItem2Loaded = createMediaItem("M2") + val mediaItem2Loaded = createMediaItemWithMediaId("M2") analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemTransitionSeek(mediaItem, mediaItem2) Assert.assertEquals(listOf(EventState.IDLE, EventState.START, EventState.END), tracker.stateList) @@ -203,15 +203,15 @@ class TestCurrentMediaItemTracker { @Test fun testMediaTransitionSameItemAuto() = runTest { val expectedStates = listOf(EventState.IDLE, EventState.START, EventState.EOF, EventState.START, EventState.EOF) - val mediaItem = createMediaItem("M1") - val mediaItem2 = createMediaItem("M2") + val mediaItem = createMediaItemWithMediaId("M1") + val mediaItem2 = createMediaItemWithMediaId("M2") analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemTransitionAuto(mediaItem, mediaItem2) analyticsCommander.simulateItemEnd(mediaItem2) Assert.assertEquals("Different Item", expectedStates, tracker.stateList) tracker.clear() - val mediaItem3 = createMediaItem("M1") + val mediaItem3 = createMediaItemWithMediaId("M1") analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemTransitionAuto(mediaItem, mediaItem3) analyticsCommander.simulateItemEnd(mediaItem3) @@ -221,7 +221,7 @@ class TestCurrentMediaItemTracker { @Test fun testMediaTransitionRepeat() = runTest { val expectedStates = listOf(EventState.IDLE, EventState.START, EventState.EOF, EventState.START) - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemTransitionRepeat(mediaItem) @@ -231,7 +231,7 @@ class TestCurrentMediaItemTracker { @Test fun testMultipleStop() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemEnd(mediaItem) analyticsCommander.simulateRelease(mediaItem) @@ -242,7 +242,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartEndDisableAtStartAnalytics() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE) currentItemTracker.enabled = false analyticsCommander.simulateItemStart(mediaItem) @@ -252,7 +252,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartEndToggleAnalytics() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.END, EventState.START, EventState.EOF) currentItemTracker.enabled = true analyticsCommander.simulateItemStart(mediaItem) @@ -266,7 +266,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartAsyncLoadEndToggleAnalytics() = runTest { val mediaItemEmpty = MediaItem.Builder().setMediaId("M1").build() - val mediaItemLoaded = createMediaItem("M1") + val mediaItemLoaded = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.END, EventState.START, EventState.EOF) currentItemTracker.enabled = true analyticsCommander.simulateItemStart(mediaItemEmpty) @@ -281,7 +281,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartAsyncLoadEndDisableAtEnd() = runTest { val mediaItemEmpty = MediaItem.Builder().setMediaId("M1").build() - val mediaItemLoaded = createMediaItem("M1") + val mediaItemLoaded = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.EOF) currentItemTracker.enabled = true analyticsCommander.simulateItemStart(mediaItemEmpty) @@ -293,7 +293,7 @@ class TestCurrentMediaItemTracker { @Test fun testStartRemoveItem() = runTest { - val mediaItem = createMediaItem("M1") + val mediaItem = createMediaItemWithMediaId("M1") val expected = listOf(EventState.IDLE, EventState.START, EventState.END) analyticsCommander.simulateItemStart(mediaItem) analyticsCommander.simulateItemRemoved(mediaItem) @@ -303,9 +303,9 @@ class TestCurrentMediaItemTracker { companion object { private val uri: Uri = mockk(relaxed = true) - fun createMediaItem(mediaId: String): MediaItem { + fun createMediaItemWithMediaId(mediaId: String): MediaItem { every { uri.toString() } returns "https://host/media.mp4" - every { uri.equals(Any()) } returns true + every { uri == Any() } returns true return MediaItem.Builder() .setUri(uri) .setMediaId(mediaId) diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestMediaItemTrackerList.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestMediaItemTrackerList.kt index 8a7c79d04..93011f508 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestMediaItemTrackerList.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestMediaItemTrackerList.kt @@ -89,30 +89,21 @@ class TestMediaItemTrackerList { Assert.assertNull(trackerC) } - private class ItemTrackerA : MediaItemTracker { + private open class EmptyItemTracker : MediaItemTracker { override fun start(player: ExoPlayer, initialData: Any?) { + // Nothing } override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { + // Nothing } - } - private class ItemTrackerB : MediaItemTracker { - override fun start(player: ExoPlayer, initialData: Any?) { - } - - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { - } - } + private class ItemTrackerA : EmptyItemTracker() - private open class ItemTrackerC : MediaItemTracker { - override fun start(player: ExoPlayer, initialData: Any?) { - } + private class ItemTrackerB : EmptyItemTracker() - override fun stop(player: ExoPlayer, reason: MediaItemTracker.StopReason, positionMs: Long) { - } - } + private open class ItemTrackerC : EmptyItemTracker() private class ItemTrackerD : ItemTrackerC() } diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPlayerCallbackFlow.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPlayerCallbackFlow.kt index 2682afc72..76d059dff 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPlayerCallbackFlow.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPlayerCallbackFlow.kt @@ -12,20 +12,16 @@ import androidx.media3.common.Player import androidx.media3.common.Player.Commands import androidx.media3.common.Timeline import androidx.media3.common.VideoSize +import app.cash.turbine.test import ch.srgssr.pillarbox.player.test.utils.PlayerListenerCommander +import ch.srgssr.pillarbox.player.utils.StringUtil import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withTimeout import org.junit.After import org.junit.Assert import org.junit.Before @@ -49,264 +45,273 @@ class TestPlayerCallbackFlow { @Test fun testCurrentPositionWhilePlaying() = runTest { - val positions = listOf(C.TIME_UNSET, 0L, 1000L, 2000L, 3000L, 4000L, 5000L) - every { player.currentPosition } returnsMany positions + every { player.currentPosition } returns C.TIME_UNSET every { player.isPlaying } returns true - val currentPositionFlow = player.currentPositionAsFlow() - val actualPositions = currentPositionFlow.take(positions.size).toList() - Assert.assertEquals(positions, actualPositions) + currentPositionFlow.test { + Assert.assertEquals(C.TIME_UNSET, awaitItem()) + every { player.currentPosition } returns 1000L + Assert.assertEquals(1000L, awaitItem()) + every { player.currentPosition } returns 10_000L + Assert.assertEquals(10_000L, awaitItem()) + ensureAllEventsConsumed() + } } /** * Test current position while not playing * We expected a Timeout as the flow doesn't start */ - @Test(expected = TimeoutCancellationException::class) + @Test fun testCurrentPositionWhileNotPlaying() = runTest { - val positions = listOf(C.TIME_UNSET, 0L, 1000L, 2000L, 3000L, 4000L, 5000L) - every { player.currentPosition } returnsMany positions every { player.isPlaying } returns false - - val currentPositionFlow = player.currentPositionAsFlow() - val firstPosition = currentPositionFlow.first() - Assert.assertEquals(positions[0], firstPosition) - - withTimeout(3_000L) { - val actualPositions = currentPositionFlow.take(positions.size).toList() - Assert.assertEquals(positions, actualPositions) + every { player.currentPosition } returns 1000L + player.currentPositionAsFlow().test { + Assert.assertEquals(1000L, awaitItem()) + ensureAllEventsConsumed() } } @Test fun testCurrentBufferedPercentage() = runTest { - val bufferedPercentages = listOf(0, 5, 15, 20, 50, 60, 75, 100) - every { player.bufferedPercentage } returnsMany bufferedPercentages every { player.isPlaying } returns true - - val currentBufferedPercentageFlow = player.currentBufferedPercentageAsFlow() - val actualBufferedPositions = currentBufferedPercentageFlow.take(bufferedPercentages.size).toList() - Assert.assertEquals(bufferedPercentages.map { it / 100f }, actualBufferedPositions) + player.currentBufferedPercentageAsFlow().test { + every { player.bufferedPercentage } returns 0 + Assert.assertEquals(0.0f, awaitItem()) + every { player.bufferedPercentage } returns 75 + Assert.assertEquals(0.75f, awaitItem()) + ensureAllEventsConsumed() + } } - @Test(timeout = 5_000) - fun testUpdateCurrentPositionAfterSeek() = runTest { - val positions = listOf(0L, 1000L, 2000L) - every { player.isPlaying } returns false // TO disable periodic update - every { player.currentPosition } returnsMany positions + @Test + fun testUpdateCurrentPositionAfterPositionDiscontinuity() = runTest { + every { player.isPlaying } returns false // disable periodic update + every { player.currentPosition } returns 0L val fakePlayer = PlayerListenerCommander(player) - val playbackSpeedFlow = fakePlayer.currentPositionAsFlow() - val actualPositions = ArrayList() - val job = launch(dispatcher) { - playbackSpeedFlow.take(positions.size).toList(actualPositions) - } - - fakePlayer.onPositionDiscontinuity( - mockk(), - mockk(), - Player.DISCONTINUITY_REASON_SEEK + val discontinuityTests = mapOf( + Player.DISCONTINUITY_REASON_SEEK to 1000L, + Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT to 2000L, + Player.DISCONTINUITY_REASON_INTERNAL to 3000L, + Player.DISCONTINUITY_REASON_AUTO_TRANSITION to 4000L, + Player.DISCONTINUITY_REASON_REMOVE to 0L, + Player.DISCONTINUITY_REASON_SKIP to C.TIME_UNSET, ) - - fakePlayer.onPositionDiscontinuity( - mockk(), - mockk(), - Player.DISCONTINUITY_REASON_SEEK - ) - - Assert.assertEquals(positions, actualPositions) - job.cancel() + fakePlayer.currentPositionAsFlow().test { + Assert.assertEquals(0L, awaitItem()) + for ((reason, position) in discontinuityTests) { + fakePlayer.onPositionDiscontinuity( + mockk(), + Player.PositionInfo(null, 0, null, null, 0, position, 0, 0, 0), + reason + ) + } + + for ((reason, position) in discontinuityTests) { + Assert.assertEquals(StringUtil.discontinuityReasonString(reason), position, awaitItem()) + } + ensureAllEventsConsumed() + } } - @Test(timeout = 10_000L) + @Test fun testDuration() = runTest { - val durations = listOf(1_000L, 5_000L, 10_000L, 20_000L) - every { player.duration } returnsMany durations - + every { player.duration } returns C.TIME_UNSET val fakePlayer = PlayerListenerCommander(player) val durationFlow = fakePlayer.durationAsFlow() - - val actualDuration = ArrayList() - val job = launch(dispatcher) { - durationFlow.take(durations.size).toList(actualDuration) + durationFlow.test { + every { player.duration } returns 20_000L + fakePlayer.onPlaybackStateChanged(Player.STATE_BUFFERING) + fakePlayer.onPlaybackStateChanged(Player.STATE_READY) + every { player.duration } returns 30_000L + fakePlayer.onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) + every { player.duration } returns 40_000L + fakePlayer.onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + + Assert.assertEquals("initial duration", C.TIME_UNSET, awaitItem()) + Assert.assertEquals("State ready", 20_000L, awaitItem()) + Assert.assertEquals("TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED", 30_000L, awaitItem()) + Assert.assertEquals("TIMELINE_CHANGE_REASON_SOURCE_UPDATE", 40_000L, awaitItem()) + ensureAllEventsConsumed() } - fakePlayer.onPlaybackStateChanged(Player.STATE_READY) - fakePlayer.onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) - fakePlayer.onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - Assert.assertEquals(durations, actualDuration) - job.cancel() } - @Test(timeout = 2_000) + @Test fun testIsPlaying() = runTest { every { player.isPlaying } returns false val fakePlayer = PlayerListenerCommander(player) - val isPlayFlow = fakePlayer.isPlayingAsFlow() - - val isPlaying = listOf(false, false, true, false, true) - val actualIsPlaying = ArrayList() - val job = launch(dispatcher) { - isPlayFlow.take(isPlaying.size).toList(actualIsPlaying) - } - for (playing in listOf(false, true, false, true)) { - fakePlayer.onIsPlayingChanged(playing) + val isPlayingFlow = fakePlayer.isPlayingAsFlow() + isPlayingFlow.test { + fakePlayer.onIsPlayingChanged(true) + fakePlayer.onIsPlayingChanged(true) + fakePlayer.onIsPlayingChanged(false) + + Assert.assertEquals("initial isPlaying", false, awaitItem()) + Assert.assertEquals("isPlaying", true, awaitItem()) + Assert.assertEquals("isPlaying", true, awaitItem()) + Assert.assertEquals("isPlaying", false, awaitItem()) + ensureAllEventsConsumed() } - Assert.assertEquals(isPlaying, actualIsPlaying) - job.cancel() } - @Test(timeout = 5_000) + @Test fun testPlaybackState() = runTest { every { player.playbackState } returns Player.STATE_IDLE val fakePlayer = PlayerListenerCommander(player) val playbackStateFlow = fakePlayer.playbackStateAsFlow() - val actualPlaybackStates = ArrayList() val playbackStates = listOf( - Player.STATE_IDLE, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED ) - val job = launch(dispatcher) { - playbackStateFlow.take(playbackStates.size).toList(actualPlaybackStates) + playbackStateFlow.test { + for (state in playbackStates) { + fakePlayer.onPlaybackStateChanged(state) + } + + Assert.assertEquals("Initial state", Player.STATE_IDLE, awaitItem()) + for (state in playbackStates) { + Assert.assertEquals(StringUtil.playerStateString(state), state, awaitItem()) + } + ensureAllEventsConsumed() } - - fakePlayer.onPlaybackStateChanged(Player.STATE_BUFFERING) - fakePlayer.onPlaybackStateChanged(Player.STATE_READY) - fakePlayer.onPlaybackStateChanged(Player.STATE_BUFFERING) - fakePlayer.onPlaybackStateChanged(Player.STATE_READY) - fakePlayer.onPlaybackStateChanged(Player.STATE_ENDED) - - Assert.assertEquals(playbackStates, actualPlaybackStates) - job.cancel() } - @Test(timeout = 2_000) - fun testError() = runTest { + @Test + fun testPlaybackError() = runTest { val error = mockk() val noError: PlaybackException? = null every { player.playerError } returns null val fakePlayer = PlayerListenerCommander(player) - val errorFlow = fakePlayer.playerErrorAsFlow() - val actualErrors = ArrayList() - val errors = - listOf( - noError, error, noError - ) - val job = launch(dispatcher) { - errorFlow.take(errors.size).toList(actualErrors) - } - fakePlayer.onPlayerErrorChanged(error) - fakePlayer.onPlayerErrorChanged(noError) + fakePlayer.playerErrorAsFlow().test { + fakePlayer.onPlayerErrorChanged(error) + fakePlayer.onPlayerErrorChanged(noError) - Assert.assertEquals(errors, actualErrors) - job.cancel() + Assert.assertEquals("Initial error", null, awaitItem()) + Assert.assertEquals("error", error, awaitItem()) + Assert.assertEquals("error removed", noError, awaitItem()) + ensureAllEventsConsumed() + } } - @Test(timeout = 2_000) + @Test fun testAvailableCommands() = runTest { val command1 = mockk() val command2 = mockk() every { player.availableCommands } returns command1 val fakePlayer = PlayerListenerCommander(player) - val commandsFlow = fakePlayer.availableCommandsAsFlow() - val actualErrors = ArrayList() - val commands = listOf(command1, command2) - val job = launch(dispatcher) { - commandsFlow.take(commands.size).toList(actualErrors) + fakePlayer.availableCommandsAsFlow().test { + fakePlayer.onAvailableCommandsChanged(command2) + Assert.assertEquals(command1, awaitItem()) + Assert.assertEquals(command2, awaitItem()) + ensureAllEventsConsumed() } - - fakePlayer.onAvailableCommandsChanged(command2) - - Assert.assertEquals(commands, actualErrors) - job.cancel() } @Test fun testShuffleModeEnabled() = runTest { every { player.shuffleModeEnabled } returns false - val listener = PlayerListenerCommander(player) - - val shuffleModeEnabledFlow = listener.shuffleModeEnabledAsFlow() - val shuffleModeEnabled = listOf(false, false, true, false, true) - val actualShuffleModeEnabled = mutableListOf() - - val job = launch(dispatcher) { - shuffleModeEnabledFlow.take(shuffleModeEnabled.size).toList(actualShuffleModeEnabled) + val fakePlayer = PlayerListenerCommander(player) + fakePlayer.shuffleModeEnabledAsFlow().test { + fakePlayer.onShuffleModeEnabledChanged(false) + fakePlayer.onShuffleModeEnabledChanged(false) + fakePlayer.onShuffleModeEnabledChanged(true) + fakePlayer.onShuffleModeEnabledChanged(false) + + Assert.assertEquals("initial state", false, awaitItem()) + Assert.assertEquals(false, awaitItem()) + Assert.assertEquals(false, awaitItem()) + Assert.assertEquals(true, awaitItem()) + Assert.assertEquals(false, awaitItem()) + ensureAllEventsConsumed() } - - listOf(false, true, false, true) - .forEach(listener::onShuffleModeEnabledChanged) - - Assert.assertEquals(shuffleModeEnabled, actualShuffleModeEnabled) - - job.cancel() } - @Test(timeout = 2_000) + @Test fun testPlaybackSpeed() = runTest { val initialPlaybackRate = 1.5f val initialParameters: PlaybackParameters = PlaybackParameters.DEFAULT.withSpeed(initialPlaybackRate) every { player.playbackParameters } returns initialParameters val fakePlayer = PlayerListenerCommander(player) - val playbackSpeedFlow = fakePlayer.getPlaybackSpeedAsFlow() - val actualSpeeds = ArrayList() - val speeds = listOf(initialPlaybackRate, 2.0f) - val job = launch(dispatcher) { - playbackSpeedFlow.take(speeds.size).toList(actualSpeeds) + fakePlayer.getPlaybackSpeedAsFlow().test { + fakePlayer.onPlaybackParametersChanged(initialParameters.withSpeed(2.0f)) + fakePlayer.onPlaybackParametersChanged(initialParameters.withSpeed(0.5f)) + + Assert.assertEquals("Initial playback speed", initialPlaybackRate, awaitItem()) + Assert.assertEquals(2.0f, awaitItem()) + Assert.assertEquals(0.5f, awaitItem()) + ensureAllEventsConsumed() } - - fakePlayer.onPlaybackParametersChanged(initialParameters.withSpeed(2.0f)) - - Assert.assertEquals(speeds, actualSpeeds) - job.cancel() } - @Test(timeout = 5_000) + @Test fun testCurrentMediaIndex() = runTest { - val indices = listOf(1, 1, 2) - every { player.currentMediaItemIndex } returnsMany indices + every { player.currentMediaItemIndex } returns 0 val fakePlayer = PlayerListenerCommander(player) - val currentIndexFlow = fakePlayer.getCurrentMediaItemIndexAsFlow() - val actualIndices = ArrayList() - val job = launch(dispatcher) { - currentIndexFlow.take(indices.size).toList(actualIndices) + val transitionReasonCases = mapOf( + Player.MEDIA_ITEM_TRANSITION_REASON_SEEK to 10, + Player.MEDIA_ITEM_TRANSITION_REASON_AUTO to 11, + Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT to 12, + Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED to 13, + ) + fakePlayer.getCurrentMediaItemIndexAsFlow().test { + every { player.currentMediaItemIndex } returns 78 + fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + every { player.currentMediaItemIndex } returns 2 + fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) + + for ((reason, index) in transitionReasonCases) { + every { player.currentMediaItemIndex } returns index + fakePlayer.onMediaItemTransition(mockk(), reason) + } + + Assert.assertEquals("Initial index", 0, awaitItem()) + Assert.assertEquals("TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED", 2, awaitItem()) + for ((reason, index) in transitionReasonCases) { + Assert.assertEquals(StringUtil.mediaItemTransitionReasonString(reason), index, awaitItem()) + } + ensureAllEventsConsumed() } - - fakePlayer.onMediaItemTransition(mockk(), Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) - fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) - - Assert.assertEquals(indices, actualIndices) - job.cancel() } - @Test(timeout = 5_000) + @Test fun testCurrentMediaItem() = runTest { - val mediaItem1: MediaItem? = null - val mediaItem2: MediaItem = mockk() - val mediaItem3: MediaItem = mockk() - val mediaItems = listOf(mediaItem1, mediaItem2, mediaItem3) - every { player.currentMediaItem } returnsMany listOf(mediaItem1, mediaItem3) + every { player.currentMediaItem } returns null val fakePlayer = PlayerListenerCommander(player) - val currentMediaItemFlow = fakePlayer.currentMediaItemAsFlow() - val actualMediaItems = ArrayList() - val job = launch(dispatcher) { - currentMediaItemFlow.take(mediaItems.size).toList(actualMediaItems) + val transitionReasonCases = mapOf( + Player.MEDIA_ITEM_TRANSITION_REASON_SEEK to mockk(), + Player.MEDIA_ITEM_TRANSITION_REASON_AUTO to mockk(), + Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT to mockk(), + Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED to mockk(), + ) + val mediaItemTimeLinePlaylistChanged: MediaItem = mockk() + val mediaItemTimeLineSourceUpdate: MediaItem = mockk() + fakePlayer.currentMediaItemAsFlow().test { + every { player.currentMediaItem } returns mediaItemTimeLinePlaylistChanged + fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) + every { player.currentMediaItem } returns mediaItemTimeLineSourceUpdate + fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) + + for ((reason, mediaItem) in transitionReasonCases) { + every { player.currentMediaItem } returns mediaItem + fakePlayer.onMediaItemTransition(mediaItem, reason) + } + + Assert.assertNull("Initial", awaitItem()) + Assert.assertEquals("TIMELINE_CHANGE_REASON_SOURCE_UPDATE", mediaItemTimeLineSourceUpdate, awaitItem()) + for ((reason, mediaItem) in transitionReasonCases) { + Assert.assertEquals(StringUtil.mediaItemTransitionReasonString(reason), mediaItem, awaitItem()) + } + ensureAllEventsConsumed() } - - fakePlayer.onMediaItemTransition(mediaItem2, Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) - fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) - fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - - Assert.assertEquals(mediaItems, actualMediaItems) - job.cancel() } - @Test(timeout = 5_000) + @Test fun testCurrentMediaItems() = runTest { val item1 = mockk() val item2 = mockk() @@ -314,46 +319,39 @@ class TestPlayerCallbackFlow { val list1 = listOf(item1, item2) val list2 = listOf(item1, item2, item3) - every { player.mediaItemCount } returnsMany listOf(2, 2, 3, 3) // read twice in getCurrentMediaItems! + + every { player.mediaItemCount } returns 2 every { player.getMediaItemAt(0) } returns item1 every { player.getMediaItemAt(1) } returns item2 every { player.getMediaItemAt(2) } returns item3 val fakePlayer = PlayerListenerCommander(player) - Assert.assertEquals(item1, fakePlayer.getMediaItemAt(0)) - Assert.assertEquals(item2, fakePlayer.getMediaItemAt(1)) - Assert.assertEquals(item3, fakePlayer.getMediaItemAt(2)) + fakePlayer.getCurrentMediaItemsAsFlow().test { + every { player.mediaItemCount } returns 3 + fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) + every { player.mediaItemCount } returns 1 + fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) - val currentMediaItemFlow = fakePlayer.getCurrentMediaItemsAsFlow() - val actualMediaItems = ArrayList>() - val job = launch(dispatcher) { - currentMediaItemFlow.take(2).toList(actualMediaItems) + Assert.assertEquals("Initial list", list1, awaitItem()) + Assert.assertEquals("TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED", list2, awaitItem()) + Assert.assertEquals("TIMELINE_CHANGE_REASON_SOURCE_UPDATE list", listOf(item1), awaitItem()) + ensureAllEventsConsumed() } - fakePlayer.onTimelineChanged(mockk(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) - - Assert.assertEquals(2, actualMediaItems.size) - Assert.assertEquals(list1, actualMediaItems[0]) - Assert.assertEquals(list2, actualMediaItems[1]) - job.cancel() } - @Test(timeout = 2_000) + @Test fun testVideoSize() = runTest { val initialSize = VideoSize.UNKNOWN val newSize = VideoSize(1200, 1000) every { player.videoSize } returns initialSize val fakePlayer = PlayerListenerCommander(player) - val videoSize = fakePlayer.videoSizeAsFlow() - val actualVideoSize = ArrayList() - val videoSizes = listOf(initialSize, newSize) - val job = launch(dispatcher) { - videoSize.take(videoSizes.size).toList(actualVideoSize) - } - - fakePlayer.onVideoSizeChanged(newSize) + fakePlayer.videoSizeAsFlow().test { + fakePlayer.onVideoSizeChanged(newSize) - Assert.assertEquals(videoSizes, actualVideoSize) - job.cancel() + Assert.assertEquals("Initial video size", initialSize, awaitItem()) + Assert.assertEquals("Updated video size", newSize, awaitItem()) + ensureAllEventsConsumed() + } } }