Skip to content

Improve audio focus management #4707

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

Merged
merged 12 commits into from
May 13, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,13 @@ class ElementCallActivity :
}

private fun setCallIsActive() {
audioFocus.requestAudioFocus(AudioFocusRequester.ElementCall)
audioFocus.requestAudioFocus(
mode = AudioFocusRequester.ElementCall,
onFocusLost = {
// If the audio focus is lost, we do not stop the call.
Timber.tag(loggerTag.value).w("Audio focus lost")
}
)
CallForegroundService.start(this)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ enum class AudioFocusRequester {
}

interface AudioFocus {
fun requestAudioFocus(mode: AudioFocusRequester)
fun requestAudioFocus(
mode: AudioFocusRequester,
onFocusLost: () -> Unit,
)

fun releaseAudioFocus()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,34 @@ class DefaultAudioFocus @Inject constructor(
private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null

@Suppress("DEPRECATION")
override fun requestAudioFocus(mode: AudioFocusRequester) {
override fun requestAudioFocus(
mode: AudioFocusRequester,
onFocusLost: () -> Unit,
) {
val listener = AudioManager.OnAudioFocusChangeListener {
when (it) {
AudioManager.AUDIOFOCUS_GAIN -> {
// Do nothing
}
AudioManager.AUDIOFOCUS_LOSS,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe ignore it for the _CAN_DUCK variant if the requester is the media viewer? https://developer.android.com/media/optimize/audio-focus#automatic-ducking

Apparently the OS will automatically duck the audio in that case, but we shouldn't do it for voice-related audio (messages, call).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the doc again, I think I can improve the code behavior yes. I will add more commits.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so I was about to check the result of the audio focus requester to prevent the start of the play back, but it seems that (well, on my phone at least) when there is an active call, the audio is ducked automatically by the system. Other apps does not always prevent playback during a phone call, for instance:

  • Netflix: cannot play
  • Youtube and Spotify: can start playback, but ducked.

So without doing anything, Element will have the same behavior than Youtube and Spotify, which is probably acceptable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me! Thanks for the changes and the documentation.

onFocusLost()
}
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(mode.toAudioUsage())
.build()
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.setOnAudioFocusChangeListener(listener)
.build()
audioManager.requestAudioFocus(request)
audioFocusRequest = request
} else {
val listener = AudioManager.OnAudioFocusChangeListener { }
audioManager.requestAudioFocus(
listener,
mode.toAudioStream(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import io.element.android.libraries.audio.api.AudioFocusRequester
import io.element.android.tests.testutils.lambda.lambdaError

class FakeAudioFocusRequester(
private val requestAudioFocusResult: (AudioFocusRequester) -> Unit = { lambdaError() },
private val requestAudioFocusResult: (AudioFocusRequester, () -> Unit) -> Unit = { _, _ -> lambdaError() },
private val releaseAudioFocusResult: () -> Unit = { lambdaError() },
) : AudioFocus {
override fun requestAudioFocus(mode: AudioFocusRequester) {
requestAudioFocusResult(mode)
override fun requestAudioFocus(
mode: AudioFocusRequester,
onFocusLost: () -> Unit,
) {
requestAudioFocusResult(mode, onFocusLost)
}

override fun releaseAudioFocus() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,14 @@ class DefaultMediaPlayer @Inject constructor(
}

override fun play() {
audioFocus.requestAudioFocus(AudioFocusRequester.VoiceMessage)
audioFocus.requestAudioFocus(
mode = 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface SimplePlayer {
fun getCurrentMediaItem(): MediaItem?
fun prepare()
fun play()
fun isPlaying(): Boolean
fun pause()
fun seekTo(positionMs: Long)
fun release()
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() },
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import io.element.android.libraries.designsystem.theme.components.Slider
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.mediaviewer.impl.util.bgCanvasWithTransparency
import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber

@Composable
fun MediaPlayerControllerView(
Expand All @@ -57,7 +58,13 @@ fun MediaPlayerControllerView(
if (audioFocus != null) {
LaunchedEffect(state.isPlaying) {
if (state.isPlaying) {
audioFocus.requestAudioFocus(AudioFocusRequester.MediaViewer)
audioFocus.requestAudioFocus(
mode = AudioFocusRequester.MediaViewer,
onFocusLost = {
Timber.w("Audio focus lost")
onTogglePlay()
},
)
} else {
audioFocus.releaseAudioFocus()
}
Expand Down