From 799fb1a67628548828ef9a36e2895a153f009063 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Dec 2024 10:07:51 +0100 Subject: [PATCH] Add test on DefaultMediaPlayer. --- .../mediaplayer/impl/DefaultMediaPlayer.kt | 12 +- .../impl/DefaultMediaPlayerTest.kt | 388 +++++++++++++++++- .../mediaplayer/impl/FakeSimplePlayer.kt | 58 +++ 3 files changed, 449 insertions(+), 9 deletions(-) create mode 100644 libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt index c013ddd587..45fc226cd6 100644 --- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt @@ -15,7 +15,6 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -36,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds @SingleIn(RoomScope::class) class DefaultMediaPlayer @Inject constructor( private val player: SimplePlayer, + private val coroutineScope: CoroutineScope, ) : MediaPlayer { private val listener = object : SimplePlayer.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { @@ -47,7 +47,7 @@ class DefaultMediaPlayer @Inject constructor( ) } if (isPlaying) { - job = scope.launch { updateCurrentPosition() } + job = coroutineScope.launch { updateCurrentPosition() } } else { job?.cancel() } @@ -79,7 +79,6 @@ class DefaultMediaPlayer @Inject constructor( player.addListener(listener) } - private val scope = CoroutineScope(Job() + Dispatchers.Main) private var job: Job? = null private val _state = MutableStateFlow( @@ -102,7 +101,8 @@ class DefaultMediaPlayer @Inject constructor( mimeType: String, startPositionMs: Long, ): MediaPlayer.State { - player.pause() // Must pause here otherwise if the player was playing it would keep on playing the new media item. + // Must pause here otherwise if the player was playing it would keep on playing the new media item. + player.pause() player.clearMediaItems() player.setMediaItem( MediaItem.Builder() @@ -129,11 +129,9 @@ class DefaultMediaPlayer @Inject constructor( player.getCurrentMediaItem()?.let { player.setMediaItem(it, 0) player.prepare() - player.play() } - } else { - player.play() } + player.play() } override fun pause() { diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt index 16242badb7..5262d65f3d 100644 --- a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt +++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt @@ -7,12 +7,396 @@ package io.element.android.libraries.mediaplayer.impl +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.mediaplayer.api.MediaPlayer +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertThrows import org.junit.Test class DefaultMediaPlayerTest { + private val aMediaId = "mediaId" + private val aMediaItem = MediaItem.Builder().setMediaId(aMediaId).build() + @Test - fun `default test`() = runTest { - // TODO + fun `initial state`() = runTest { + val sut = createDefaultMediaPlayer() + sut.state.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = null, + ) + ) + } } + + @Test + fun `start player will update the current position and pause it will stop`() = runTest { + val playLambda = lambdaRecorder { } + val pauseLambda = lambdaRecorder { } + val player = FakeSimplePlayer( + playLambda = playLambda, + pauseLambda = pauseLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.state.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = null, + ) + ) + sut.play() + playLambda.assertions().isCalledOnce() + player.durationResult = 123L + player.simulateIsPlayingChanged(true) + val playingState = awaitItem() + assertThat(playingState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = true, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = 123, + ) + ) + player.currentPositionResult = 1L + assertThat(awaitItem()).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = true, + isEnded = false, + mediaId = null, + currentPosition = 1, + duration = 123, + ) + ) + player.currentPositionResult = 2L + assertThat(awaitItem()).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = true, + isEnded = false, + mediaId = null, + currentPosition = 2, + duration = 123, + ) + ) + player.pause() + pauseLambda.assertions().isCalledOnce() + player.simulateIsPlayingChanged(false) + assertThat(awaitItem()).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 2, + duration = 123, + ) + ) + } + } + + @Test + fun `start player on ended playback will not invoke more methods if current media item is null`() = runTest { + val playLambda = lambdaRecorder { } + val getCurrentMediaItemLambda = lambdaRecorder { null } + val player = FakeSimplePlayer( + playLambda = playLambda, + getCurrentMediaItemLambda = getCurrentMediaItemLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.state.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = null, + ) + ) + player.playbackStateResult = Player.STATE_ENDED + sut.play() + playLambda.assertions().isCalledOnce() + } + } + + @Test + fun `start player on ended playback will invoke more methods if current media item is not null`() = runTest { + val playLambda = lambdaRecorder { } + val prepareLambda = lambdaRecorder { } + val getCurrentMediaItemLambda = lambdaRecorder { aMediaItem } + val setMediaItemLambda = lambdaRecorder { _, _ -> } + val player = FakeSimplePlayer( + playLambda = playLambda, + prepareLambda = prepareLambda, + setMediaItemLambda = setMediaItemLambda, + getCurrentMediaItemLambda = getCurrentMediaItemLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.state.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = null, + ) + ) + player.playbackStateResult = Player.STATE_ENDED + sut.play() + setMediaItemLambda.assertions().isCalledOnce().with( + value(aMediaItem), + value(0L), + ) + prepareLambda.assertions().isCalledOnce() + playLambda.assertions().isCalledOnce() + } + } + + @Test + fun `pause player invokes pause on the embedded player`() = runTest { + val pauseLambda = lambdaRecorder { } + val player = FakeSimplePlayer( + pauseLambda = pauseLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.pause() + pauseLambda.assertions().isCalledOnce() + } + + @Test + fun `close player invokes release on the embedded player`() = runTest { + val releaseLambda = lambdaRecorder { } + val player = FakeSimplePlayer( + releaseLambda = releaseLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.close() + releaseLambda.assertions().isCalledOnce() + } + + @Test + fun `seekTo invokes release on the embedded player`() = runTest { + val seekToLambda = lambdaRecorder { } + val player = FakeSimplePlayer( + seekToLambda = seekToLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.state.test { + awaitItem() + player.currentPositionResult = 33L + sut.seekTo(33L) + seekToLambda.assertions().isCalledOnce().with(value(33L)) + val finalState = awaitItem() + assertThat(finalState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 33L, + duration = null, + ) + ) + } + } + + @Test + fun `onPlaybackStateChanged update the state`() = runTest { + val player = FakeSimplePlayer() + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.state.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = null, + ) + ) + player.currentPositionResult = 44 + player.durationResult = 123L + player.simulatePlaybackStateChanged(Player.STATE_READY) + val readyState = awaitItem() + assertThat(readyState).isEqualTo( + MediaPlayer.State( + isReady = true, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 44, + duration = 123, + ) + ) + player.simulatePlaybackStateChanged(Player.STATE_ENDED) + val endedState = awaitItem() + assertThat(endedState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = true, + mediaId = null, + currentPosition = 44, + duration = 123, + ) + ) + } + } + + @Test + fun `setMedia with timeout error`() = runTest { + val pauseLambda = lambdaRecorder { } + val clearMediaItemsLambda = lambdaRecorder { } + val setMediaItemLambda = lambdaRecorder { _, _ -> } + val prepareLambda = lambdaRecorder { } + val player = FakeSimplePlayer( + pauseLambda = pauseLambda, + clearMediaItemsLambda = clearMediaItemsLambda, + setMediaItemLambda = setMediaItemLambda, + prepareLambda = prepareLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.state.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = null, + ) + ) + val result = runCatching { + sut.setMedia("uri", "mediaId", "mimeType", 12) + } + pauseLambda.assertions().isCalledOnce() + clearMediaItemsLambda.assertions().isCalledOnce() + setMediaItemLambda.assertions().isCalledOnce().with( + value(MediaItem.Builder().setUri("uri").setMediaId("mediaId").setMimeType("mimeType").build()), + value(12L), + ) + prepareLambda.assertions().isCalledOnce() + assertThat(result.isFailure).isTrue() + assertThrows(TimeoutCancellationException::class.java) { + result.getOrThrow() + } + } + } + + @Test + fun `setMedia success`() = runTest { + var player: FakeSimplePlayer? = null + val pauseLambda = lambdaRecorder { } + val clearMediaItemsLambda = lambdaRecorder { } + val setMediaItemLambda = lambdaRecorder { _, _ -> } + val prepareLambda = lambdaRecorder { + player?.simulatePlaybackStateChanged(Player.STATE_READY) + player?.simulateMediaItemTransition(aMediaItem) + } + player = FakeSimplePlayer( + pauseLambda = pauseLambda, + clearMediaItemsLambda = clearMediaItemsLambda, + setMediaItemLambda = setMediaItemLambda, + prepareLambda = prepareLambda, + ) + val sut = createDefaultMediaPlayer( + simplePlayer = player, + ) + sut.state.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo( + MediaPlayer.State( + isReady = false, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = null, + ) + ) + val state = sut.setMedia("uri", "mediaId", "mimeType", 12) + pauseLambda.assertions().isCalledOnce() + clearMediaItemsLambda.assertions().isCalledOnce() + setMediaItemLambda.assertions().isCalledOnce().with( + value(MediaItem.Builder().setUri("uri").setMediaId("mediaId").setMimeType("mimeType").build()), + value(12L), + ) + prepareLambda.assertions().isCalledOnce() + + val finalState = MediaPlayer.State( + isReady = true, + isPlaying = false, + isEnded = false, + mediaId = "mediaId", + currentPosition = 0, + duration = 0, + ) + assertThat(awaitItem()).isEqualTo( + MediaPlayer.State( + isReady = true, + isPlaying = false, + isEnded = false, + mediaId = null, + currentPosition = 0, + duration = 0, + ) + ) + assertThat(awaitItem()).isEqualTo(finalState) + assertThat(state).isEqualTo(finalState) + } + } + + private fun TestScope.createDefaultMediaPlayer( + simplePlayer: SimplePlayer = FakeSimplePlayer(), + ): DefaultMediaPlayer = DefaultMediaPlayer( + simplePlayer, + backgroundScope, + ) } diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt new file mode 100644 index 0000000000..d981fa7796 --- /dev/null +++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.mediaplayer.impl + +import androidx.media3.common.MediaItem +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeSimplePlayer( + private val clearMediaItemsLambda: () -> Unit = { lambdaError() }, + private val setMediaItemLambda: (MediaItem, Long) -> Unit = { _, _ -> lambdaError() }, + private val getCurrentMediaItemLambda: () -> MediaItem? = { lambdaError() }, + private val prepareLambda: () -> Unit = { lambdaError() }, + private val playLambda: () -> Unit = { lambdaError() }, + private val pauseLambda: () -> Unit = { lambdaError() }, + private val seekToLambda: (Long) -> Unit = { lambdaError() }, + private val releaseLambda: () -> Unit = { lambdaError() }, +) : SimplePlayer { + private val listeners = mutableListOf() + override fun addListener(listener: SimplePlayer.Listener) { + listeners.add(listener) + } + + var currentPositionResult: Long = 0 + override val currentPosition: Long get() = currentPositionResult + var playbackStateResult: Int = 0 + override val playbackState: Int get() = playbackStateResult + var durationResult: Long = 0 + override val duration: Long get() = durationResult + + override fun clearMediaItems() = clearMediaItemsLambda() + override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) { + setMediaItemLambda(mediaItem, startPositionMs) + } + + override fun getCurrentMediaItem(): MediaItem? = getCurrentMediaItemLambda() + override fun prepare() = prepareLambda() + override fun play() = playLambda() + override fun pause() = pauseLambda() + override fun seekTo(positionMs: Long) = seekToLambda(positionMs) + override fun release() = releaseLambda() + + fun simulateIsPlayingChanged(isPlaying: Boolean) { + listeners.forEach { it.onIsPlayingChanged(isPlaying) } + } + + fun simulateMediaItemTransition(mediaItem: MediaItem?) { + listeners.forEach { it.onMediaItemTransition(mediaItem) } + } + + fun simulatePlaybackStateChanged(playbackState: Int) { + listeners.forEach { it.onPlaybackStateChanged(playbackState) } + } +}