Add test on DefaultMediaPlayer.

This commit is contained in:
Benoit Marty
2024-12-17 10:07:51 +01:00
committed by Benoit Marty
parent f0339bc252
commit 799fb1a676
3 changed files with 449 additions and 9 deletions

View File

@@ -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() {

View File

@@ -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<Unit> { }
val pauseLambda = lambdaRecorder<Unit> { }
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<Unit> { }
val getCurrentMediaItemLambda = lambdaRecorder<MediaItem?> { 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<Unit> { }
val prepareLambda = lambdaRecorder<Unit> { }
val getCurrentMediaItemLambda = lambdaRecorder<MediaItem?> { aMediaItem }
val setMediaItemLambda = lambdaRecorder<MediaItem, Long, Unit> { _, _ -> }
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<Unit> { }
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<Unit> { }
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<Long, Unit> { }
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<Unit> { }
val clearMediaItemsLambda = lambdaRecorder<Unit> { }
val setMediaItemLambda = lambdaRecorder<MediaItem, Long, Unit> { _, _ -> }
val prepareLambda = lambdaRecorder<Unit> { }
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<Unit> { }
val clearMediaItemsLambda = lambdaRecorder<Unit> { }
val setMediaItemLambda = lambdaRecorder<MediaItem, Long, Unit> { _, _ -> }
val prepareLambda = lambdaRecorder<Unit> {
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,
)
}

View File

@@ -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<SimplePlayer.Listener>()
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) }
}
}