Improve audio focus management (#4707)
* Extract Audio focus management to its own modules. * Request Audio focus when playing a voice message. * Add missing dependency. (and remove a duplicated one) * Request Audio focus when playing a video/audio in the media viewer. * Pause audio when audio focus is lost. * Rename class * Fix tests * Fix detekt issue. * Audio focus: let the system handle automatic ducking when playing media. * Document and update API * Remove useless space.
This commit is contained in:
parent
3cf8b04a07
commit
93499910ba
24 changed files with 341 additions and 49 deletions
|
|
@ -11,6 +11,8 @@ import androidx.media3.common.C
|
|||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.audio.api.AudioFocus
|
||||
import io.element.android.libraries.audio.api.AudioFocusRequester
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
|
|
@ -36,6 +38,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
class DefaultMediaPlayer @Inject constructor(
|
||||
private val player: SimplePlayer,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val audioFocus: AudioFocus,
|
||||
) : MediaPlayer {
|
||||
private val listener = object : SimplePlayer.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
|
|
@ -49,6 +52,7 @@ class DefaultMediaPlayer @Inject constructor(
|
|||
if (isPlaying) {
|
||||
job = coroutineScope.launch { updateCurrentPosition() }
|
||||
} else {
|
||||
audioFocus.releaseAudioFocus()
|
||||
job?.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -118,6 +122,14 @@ class DefaultMediaPlayer @Inject constructor(
|
|||
}
|
||||
|
||||
override fun play() {
|
||||
audioFocus.requestAudioFocus(
|
||||
requester = AudioFocusRequester.VoiceMessage,
|
||||
onFocusLost = {
|
||||
if (player.isPlaying()) {
|
||||
player.pause()
|
||||
}
|
||||
},
|
||||
)
|
||||
if (player.playbackState == Player.STATE_ENDED) {
|
||||
// There's a bug with some ogg files that somehow report to
|
||||
// have no duration.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ interface SimplePlayer {
|
|||
fun getCurrentMediaItem(): MediaItem?
|
||||
fun prepare()
|
||||
fun play()
|
||||
fun isPlaying(): Boolean
|
||||
fun pause()
|
||||
fun seekTo(positionMs: Long)
|
||||
fun release()
|
||||
|
|
@ -80,6 +81,8 @@ class DefaultSimplePlayer(
|
|||
|
||||
override fun play() = p.play()
|
||||
|
||||
override fun isPlaying() = p.isPlaying
|
||||
|
||||
override fun pause() = p.pause()
|
||||
|
||||
override fun seekTo(positionMs: Long) = p.seekTo(positionMs)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ 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.audio.api.AudioFocus
|
||||
import io.element.android.libraries.audio.api.AudioFocusRequester
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.libraries.mediaplayer.test.FakeAudioFocus
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
|
|
@ -50,8 +53,15 @@ class DefaultMediaPlayerTest {
|
|||
playLambda = playLambda,
|
||||
pauseLambda = pauseLambda,
|
||||
)
|
||||
val requestAudioFocusResult = lambdaRecorder<AudioFocusRequester, () -> Unit, Unit> { _, _ -> }
|
||||
val releaseAudioFocusResult = lambdaRecorder<Unit> {}
|
||||
val audioFocus = FakeAudioFocus(
|
||||
requestAudioFocusResult = requestAudioFocusResult,
|
||||
releaseAudioFocusResult = releaseAudioFocusResult
|
||||
)
|
||||
val sut = createDefaultMediaPlayer(
|
||||
simplePlayer = player,
|
||||
audioFocus = audioFocus,
|
||||
)
|
||||
sut.state.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -67,6 +77,7 @@ class DefaultMediaPlayerTest {
|
|||
)
|
||||
sut.play()
|
||||
playLambda.assertions().isCalledOnce()
|
||||
requestAudioFocusResult.assertions().isCalledOnce()
|
||||
player.durationResult = 123L
|
||||
player.simulateIsPlayingChanged(true)
|
||||
val playingState = awaitItem()
|
||||
|
|
@ -105,6 +116,7 @@ class DefaultMediaPlayerTest {
|
|||
player.pause()
|
||||
pauseLambda.assertions().isCalledOnce()
|
||||
player.simulateIsPlayingChanged(false)
|
||||
releaseAudioFocusResult.assertions().isCalledOnce()
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
MediaPlayer.State(
|
||||
isReady = false,
|
||||
|
|
@ -126,8 +138,13 @@ class DefaultMediaPlayerTest {
|
|||
playLambda = playLambda,
|
||||
getCurrentMediaItemLambda = getCurrentMediaItemLambda,
|
||||
)
|
||||
val requestAudioFocusResult = lambdaRecorder<AudioFocusRequester, () -> Unit, Unit> { _, _ -> }
|
||||
val audioFocus = FakeAudioFocus(
|
||||
requestAudioFocusResult = requestAudioFocusResult,
|
||||
)
|
||||
val sut = createDefaultMediaPlayer(
|
||||
simplePlayer = player,
|
||||
audioFocus = audioFocus,
|
||||
)
|
||||
sut.state.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -144,6 +161,7 @@ class DefaultMediaPlayerTest {
|
|||
player.playbackStateResult = Player.STATE_ENDED
|
||||
sut.play()
|
||||
playLambda.assertions().isCalledOnce()
|
||||
requestAudioFocusResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,6 +171,10 @@ class DefaultMediaPlayerTest {
|
|||
val prepareLambda = lambdaRecorder<Unit> { }
|
||||
val getCurrentMediaItemLambda = lambdaRecorder<MediaItem?> { aMediaItem }
|
||||
val setMediaItemLambda = lambdaRecorder<MediaItem, Long, Unit> { _, _ -> }
|
||||
val requestAudioFocusResult = lambdaRecorder<AudioFocusRequester, () -> Unit, Unit> { _, _ -> }
|
||||
val audioFocus = FakeAudioFocus(
|
||||
requestAudioFocusResult = requestAudioFocusResult,
|
||||
)
|
||||
val player = FakeSimplePlayer(
|
||||
playLambda = playLambda,
|
||||
prepareLambda = prepareLambda,
|
||||
|
|
@ -161,6 +183,7 @@ class DefaultMediaPlayerTest {
|
|||
)
|
||||
val sut = createDefaultMediaPlayer(
|
||||
simplePlayer = player,
|
||||
audioFocus = audioFocus,
|
||||
)
|
||||
sut.state.test {
|
||||
val initialState = awaitItem()
|
||||
|
|
@ -182,6 +205,7 @@ class DefaultMediaPlayerTest {
|
|||
)
|
||||
prepareLambda.assertions().isCalledOnce()
|
||||
playLambda.assertions().isCalledOnce()
|
||||
requestAudioFocusResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -395,8 +419,10 @@ class DefaultMediaPlayerTest {
|
|||
|
||||
private fun TestScope.createDefaultMediaPlayer(
|
||||
simplePlayer: SimplePlayer = FakeSimplePlayer(),
|
||||
audioFocus: AudioFocus = FakeAudioFocus(),
|
||||
): DefaultMediaPlayer = DefaultMediaPlayer(
|
||||
simplePlayer,
|
||||
backgroundScope,
|
||||
player = simplePlayer,
|
||||
coroutineScope = backgroundScope,
|
||||
audioFocus = audioFocus,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class FakeSimplePlayer(
|
|||
private val getCurrentMediaItemLambda: () -> MediaItem? = { lambdaError() },
|
||||
private val prepareLambda: () -> Unit = { lambdaError() },
|
||||
private val playLambda: () -> Unit = { lambdaError() },
|
||||
private val isPlayingLambda: () -> Boolean = { lambdaError() },
|
||||
private val pauseLambda: () -> Unit = { lambdaError() },
|
||||
private val seekToLambda: (Long) -> Unit = { lambdaError() },
|
||||
private val releaseLambda: () -> Unit = { lambdaError() },
|
||||
|
|
@ -40,6 +41,7 @@ class FakeSimplePlayer(
|
|||
override fun getCurrentMediaItem(): MediaItem? = getCurrentMediaItemLambda()
|
||||
override fun prepare() = prepareLambda()
|
||||
override fun play() = playLambda()
|
||||
override fun isPlaying() = isPlayingLambda()
|
||||
override fun pause() = pauseLambda()
|
||||
override fun seekTo(positionMs: Long) = seekToLambda(positionMs)
|
||||
override fun release() = releaseLambda()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue