diff --git a/changelog.d/7829.bugfix b/changelog.d/7829.bugfix
new file mode 100644
index 0000000000..705f7310f0
--- /dev/null
+++ b/changelog.d/7829.bugfix
@@ -0,0 +1 @@
+Handle exceptions when listening a voice broadcast
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 6731248f83..12207bece3 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3122,6 +3122,7 @@
You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.
Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.
You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
+ Unable to play this voice broadcast.
%1$s left
Stop live broadcasting?
diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
index 380c80775b..78aaa058e9 100644
--- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
+++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
@@ -157,6 +157,8 @@ class DefaultErrorFormatter @Inject constructor(
RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
+ is VoiceBroadcastFailure.ListeningError.UnableToPlay,
+ is VoiceBroadcastFailure.ListeningError.DownloadError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
index a7b926f29a..b5c4b4a537 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
@@ -229,6 +229,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
voiceMessageViews.renderPlaying(state)
}
is AudioMessagePlaybackTracker.Listener.State.Paused,
+ is AudioMessagePlaybackTracker.Listener.State.Error,
is AudioMessagePlaybackTracker.Listener.State.Idle -> {
voiceMessageViews.renderIdle()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index cc3a015120..3439fb1f57 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -15,9 +15,9 @@
*/
package im.vector.app.features.home.room.detail.timeline.factory
+import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
-import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
@@ -36,7 +36,6 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@@ -45,6 +44,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider,
private val colorProvider: ColorProvider,
private val drawableProvider: DrawableProvider,
+ private val errorFormatter: ErrorFormatter,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
private val playbackTracker: AudioMessagePlaybackTracker,
@@ -75,13 +75,14 @@ class VoiceBroadcastItemFactory @Inject constructor(
voiceBroadcast = voiceBroadcast,
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
duration = voiceBroadcastEventsGroup.getDuration(),
- recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
+ recorderName = params.event.senderInfo.disambiguatedDisplayName,
recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer,
playbackTracker = playbackTracker,
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
colorProvider = colorProvider,
drawableProvider = drawableProvider,
+ errorFormatter = errorFormatter,
)
return if (isRecording) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
index c34cbbc74a..c598a99af7 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
@@ -50,8 +50,11 @@ class AudioMessagePlaybackTracker @Inject constructor() {
listeners.remove(id)
}
- fun pauseAllPlaybacks() {
- listeners.keys.forEach(::pausePlayback)
+ fun unregisterListeners() {
+ listeners.forEach {
+ it.value.onUpdate(Listener.State.Idle)
+ }
+ listeners.clear()
}
/**
@@ -84,6 +87,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
}
+ fun pauseAllPlaybacks() {
+ listeners.keys.forEach(::pausePlayback)
+ }
+
fun pausePlayback(id: String) {
val state = getPlaybackState(id)
if (state is Listener.State.Playing) {
@@ -94,7 +101,14 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
fun stopPlayback(id: String) {
- setState(id, Listener.State.Idle)
+ val state = getPlaybackState(id)
+ if (state !is Listener.State.Error) {
+ setState(id, Listener.State.Idle)
+ }
+ }
+
+ fun onError(id: String, error: Throwable) {
+ setState(id, Listener.State.Error(error))
}
fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) {
@@ -116,6 +130,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
is Listener.State.Playing -> state.playbackTime
is Listener.State.Paused -> state.playbackTime
is Listener.State.Recording,
+ is Listener.State.Error,
Listener.State.Idle,
null -> null
}
@@ -126,18 +141,12 @@ class AudioMessagePlaybackTracker @Inject constructor() {
is Listener.State.Playing -> state.percentage
is Listener.State.Paused -> state.percentage
is Listener.State.Recording,
+ is Listener.State.Error,
Listener.State.Idle,
null -> null
}
}
- fun unregisterListeners() {
- listeners.forEach {
- it.value.onUpdate(Listener.State.Idle)
- }
- listeners.clear()
- }
-
companion object {
const val RECORDING_ID = "RECORDING_ID"
}
@@ -148,6 +157,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
sealed class State {
object Idle : State()
+ data class Error(val failure: Throwable) : State()
data class Playing(val playbackTime: Int, val percentage: Float) : State()
data class Paused(val playbackTime: Int, val percentage: Float) : State()
data class Recording(val amplitudeList: List) : State()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
index c6b90cdabe..7cde978e42 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
@@ -22,6 +22,7 @@ import androidx.annotation.IdRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import im.vector.app.R
+import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.tintBackground
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
@@ -48,6 +49,7 @@ abstract class AbsMessageVoiceBroadcastItem() {
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
when (state) {
+ is AudioMessagePlaybackTracker.Listener.State.Error,
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
index b788d79214..0aa2aaad3b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -20,11 +20,13 @@ import android.text.format.DateUtils
import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView
+import androidx.constraintlayout.widget.Group
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
@@ -54,6 +56,16 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
}
player.addListener(voiceBroadcast, playerListener)
+
+ playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
+ renderBackwardForwardButtons(holder, playbackState)
+ renderPlaybackError(holder, playbackState)
+ renderLiveIndicator(holder)
+ if (!isUserSeeking) {
+ holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
+ }
+ }
+
bindSeekBar(holder)
bindButtons(holder)
}
@@ -63,10 +75,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
playPauseButton.setOnClickListener {
if (player.currentVoiceBroadcast == voiceBroadcast) {
when (player.playingState) {
- VoiceBroadcastPlayer.State.PLAYING,
- VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
- VoiceBroadcastPlayer.State.PAUSED,
- VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
+ VoiceBroadcastPlayer.State.Playing,
+ VoiceBroadcastPlayer.State.Buffering -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
+ VoiceBroadcastPlayer.State.Paused,
+ is VoiceBroadcastPlayer.State.Error,
+ VoiceBroadcastPlayer.State.Idle -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
}
} else {
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
@@ -100,17 +113,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
with(holder) {
- bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
- voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
+ bufferingView.isVisible = state == VoiceBroadcastPlayer.State.Buffering
+ voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.Buffering
when (state) {
- VoiceBroadcastPlayer.State.PLAYING,
- VoiceBroadcastPlayer.State.BUFFERING -> {
+ VoiceBroadcastPlayer.State.Playing,
+ VoiceBroadcastPlayer.State.Buffering -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
}
- VoiceBroadcastPlayer.State.IDLE,
- VoiceBroadcastPlayer.State.PAUSED -> {
+ is VoiceBroadcastPlayer.State.Error,
+ VoiceBroadcastPlayer.State.Idle,
+ VoiceBroadcastPlayer.State.Paused -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
}
@@ -120,6 +134,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
}
+ private fun renderPlaybackError(holder: Holder, playbackState: State) {
+ with(holder) {
+ if (playbackState is State.Error) {
+ controlsGroup.isVisible = false
+ errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure))
+ } else {
+ errorView.isVisible = false
+ controlsGroup.isVisible = true
+ }
+ }
+ }
+
private fun bindSeekBar(holder: Holder) {
with(holder) {
remainingTimeView.text = formatRemainingTime(duration)
@@ -141,13 +167,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
}
})
}
- playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
- renderBackwardForwardButtons(holder, playbackState)
- renderLiveIndicator(holder)
- if (!isUserSeeking) {
- holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
- }
- }
}
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
@@ -187,6 +206,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata)
val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind(R.id.listenersCountMetadata)
+ val errorView by bind(R.id.errorView)
+ val controlsGroup by bind(R.id.controlsGroup)
}
companion object {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
index d3f320db7d..a8e215b4a9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
@@ -124,6 +124,7 @@ abstract class MessageVoiceItem : AbsMessageItem() {
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
when (state) {
+ is AudioMessagePlaybackTracker.Listener.State.Error,
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt
index 76b50c78ab..75863dc042 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt
@@ -16,10 +16,21 @@
package im.vector.app.features.voicebroadcast
+import android.media.MediaPlayer
+
sealed class VoiceBroadcastFailure : Throwable() {
sealed class RecordingError : VoiceBroadcastFailure() {
object NoPermission : RecordingError()
object BlockedBySomeoneElse : RecordingError()
object UserAlreadyBroadcasting : RecordingError()
}
+
+ sealed class ListeningError : VoiceBroadcastFailure() {
+ /**
+ * @property what the type of error that has occurred, see [MediaPlayer.OnErrorListener.onError].
+ * @property extra an extra code, specific to the error, see [MediaPlayer.OnErrorListener.onError].
+ */
+ data class UnableToPlay(val what: Int, val extra: Int) : ListeningError()
+ data class DownloadError(override val cause: Throwable?) : ListeningError()
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
index 0de88e9992..ad0ecf69b4 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.voicebroadcast.listening
+import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
interface VoiceBroadcastPlayer {
@@ -26,7 +27,7 @@ interface VoiceBroadcastPlayer {
val currentVoiceBroadcast: VoiceBroadcast?
/**
- * The current playing [State], [State.IDLE] by default.
+ * The current playing [State], [State.Idle] by default.
*/
val playingState: State
@@ -68,11 +69,12 @@ interface VoiceBroadcastPlayer {
/**
* Player states.
*/
- enum class State {
- PLAYING,
- PAUSED,
- BUFFERING,
- IDLE
+ sealed interface State {
+ object Playing : State
+ object Paused : State
+ object Buffering : State
+ data class Error(val failure: VoiceBroadcastFailure.ListeningError) : State
+ object Idle : State
}
/**
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index 9cb894bb58..2e1600e4e2 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -24,7 +24,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.onFirst
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.session.coroutineScope
-import im.vector.app.features.voice.VoiceFailure
+import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
import im.vector.app.features.voicebroadcast.isLive
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
@@ -79,7 +79,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
}
}
- override var playingState = State.IDLE
+ override var playingState: State = State.Idle
@MainThread
set(value) {
if (field != value) {
@@ -96,7 +96,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
val hasChanged = currentVoiceBroadcast != voiceBroadcast
when {
hasChanged -> startPlayback(voiceBroadcast)
- playingState == State.PAUSED -> resumePlayback()
+ playingState == State.Paused -> resumePlayback()
else -> Unit
}
}
@@ -107,7 +107,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
override fun stop() {
// Update state
- playingState = State.IDLE
+ playingState = State.Idle
// Stop and release media players
stopPlayer()
@@ -129,7 +129,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) }
}
- listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
+ listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.Idle)
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast)
}
@@ -139,11 +139,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun startPlayback(voiceBroadcast: VoiceBroadcast) {
// Stop listening previous voice broadcast if any
- if (playingState != State.IDLE) stop()
+ if (playingState != State.Idle) stop()
currentVoiceBroadcast = voiceBroadcast
- playingState = State.BUFFERING
+ playingState = State.Buffering
observeVoiceBroadcastStateEvent(voiceBroadcast)
}
@@ -175,13 +175,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun onPlaylistUpdated() {
when (playingState) {
- State.PLAYING,
- State.PAUSED -> {
+ State.Playing,
+ State.Paused -> {
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
prepareNextMediaPlayer()
}
}
- State.BUFFERING -> {
+ State.Buffering -> {
val nextItem = if (isLiveListening && playlist.currentSequence == null) {
// live listening, jump to the last item if playback has not started
playlist.lastOrNull()
@@ -193,7 +193,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
startPlayback(nextItem.startTime)
}
}
- State.IDLE -> Unit // Should not happen
+ is State.Error -> Unit
+ State.Idle -> Unit // Should not happen
}
}
@@ -213,18 +214,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
if (sequencePosition > 0) {
mp.seekTo(sequencePosition)
}
- playingState = State.PLAYING
+ playingState = State.Playing
prepareNextMediaPlayer()
}
- } catch (failure: Throwable) {
- Timber.e(failure, "## Voice Broadcast | Unable to start playback: $failure")
- throw VoiceFailure.UnableToPlay(failure)
+ } catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) {
+ playingState = State.Error(failure)
}
}
}
private fun pausePlayback() {
- playingState = State.PAUSED // This will trigger a playing state update and save the current position
+ playingState = State.Paused // This will trigger a playing state update and save the current position
if (currentMediaPlayer != null) {
currentMediaPlayer?.pause()
} else {
@@ -234,7 +234,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun resumePlayback() {
if (currentMediaPlayer != null) {
- playingState = State.PLAYING
+ playingState = State.Playing
currentMediaPlayer?.start()
} else {
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
@@ -247,11 +247,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
voiceBroadcast != currentVoiceBroadcast -> {
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
}
- playingState == State.PLAYING || playingState == State.BUFFERING -> {
+ playingState == State.Playing || playingState == State.Buffering -> {
updateLiveListeningMode(positionMillis)
startPlayback(positionMillis)
}
- playingState == State.IDLE || playingState == State.PAUSED -> {
+ playingState == State.Idle || playingState == State.Paused -> {
stopPlayer()
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
}
@@ -263,19 +263,29 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
if (nextItem != null) {
isPreparingNextPlayer = true
sessionScope.launch {
- prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
+ try {
+ prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
+ isPreparingNextPlayer = false
+ nextMediaPlayer = mp
+ when (playingState) {
+ State.Playing,
+ State.Paused -> {
+ currentMediaPlayer?.setNextMediaPlayer(mp)
+ }
+ State.Buffering -> {
+ mp.start()
+ onNextMediaPlayerStarted(mp)
+ }
+ is State.Error,
+ State.Idle -> stopPlayer()
+ }
+ }
+ } catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) {
isPreparingNextPlayer = false
- nextMediaPlayer = mp
- when (playingState) {
- State.PLAYING,
- State.PAUSED -> {
- currentMediaPlayer?.setNextMediaPlayer(mp)
- }
- State.BUFFERING -> {
- mp.start()
- onNextMediaPlayerStarted(mp)
- }
- State.IDLE -> stopPlayer()
+ // Do not change the playingState if the current player is still valid,
+ // the error will be thrown again when switching to the next player
+ if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) {
+ playingState = State.Error(failure)
}
}
}
@@ -288,11 +298,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
session.fileService().downloadFile(messageAudioContent)
} catch (failure: Throwable) {
Timber.e(failure, "Voice Broadcast | Download has failed: $failure")
- throw VoiceFailure.UnableToPlay(failure)
+ throw VoiceBroadcastFailure.ListeningError.DownloadError(failure)
}
return audioFile.inputStream().use { fis ->
MediaPlayer().apply {
+ setOnErrorListener(mediaPlayerListener)
setAudioAttributes(
AudioAttributes.Builder()
// Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
@@ -302,10 +313,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
)
setDataSource(fis.fd)
setOnInfoListener(mediaPlayerListener)
- setOnErrorListener(mediaPlayerListener)
setOnPreparedListener(onPreparedListener)
setOnCompletionListener(mediaPlayerListener)
- prepare()
+ prepareAsync()
}
}
}
@@ -327,11 +337,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
// Start or stop playback ticker
when (playingState) {
- State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
- State.PAUSED,
- State.BUFFERING,
- State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
+ State.Playing -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
+ State.Paused,
+ State.Buffering,
+ is State.Error,
+ State.Idle -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
}
+
+ // Notify playback tracker about error
+ if (playingState is State.Error) {
+ playbackTracker.onError(voiceBroadcastId, playingState.failure)
+ }
+
// Notify state change to all the listeners attached to the current voice broadcast id
listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) }
}
@@ -348,7 +365,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// the current voice broadcast is not live (ended)
mostRecentVoiceBroadcastEvent?.isLive != true -> false
// the player is stopped or paused
- playingState == State.IDLE || playingState == State.PAUSED -> false
+ playingState == State.Idle || playingState == State.Paused -> false
seekPosition != null -> {
val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0)
val newSequence = playlist.findByPosition(seekPosition)?.sequence
@@ -374,13 +391,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun onLiveListeningChanged(isLiveListening: Boolean) {
// Live has ended and last chunk has been reached, we can stop the playback
- if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
+ val hasReachedLastChunk = playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence
+ if (!isLiveListening && playingState == State.Buffering && hasReachedLastChunk) {
stop()
}
}
private fun onNextMediaPlayerStarted(mp: MediaPlayer) {
- playingState = State.PLAYING
+ playingState = State.Playing
playlist.currentSequence = playlist.currentSequence?.inc()
currentMediaPlayer = mp
nextMediaPlayer = null
@@ -389,16 +407,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun getCurrentPlaybackPosition(): Int? {
val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null
- val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) }
+ val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlist.currentItem?.startTime?.plus(it) }
val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId)
return computedPosition ?: savedPosition
}
private fun getCurrentPlaybackPercentage(): Float? {
val playlistPosition = playlist.currentItem?.startTime
- val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
- val duration = playlist.duration.takeIf { it > 0 }
- val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null
+ val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlistPosition?.plus(it) } ?: playlistPosition
+ val duration = playlist.duration
+ val computedPercentage = if (computedPosition != null && duration > 0) computedPosition.toFloat() / duration else null
val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) }
return computedPercentage ?: savedPercentage
}
@@ -416,6 +434,14 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
}
override fun onCompletion(mp: MediaPlayer) {
+ // Release media player as soon as it completed
+ mp.release()
+ if (currentMediaPlayer == mp) {
+ currentMediaPlayer = null
+ } else {
+ error("The media player which has completed mismatches the current media player instance.")
+ }
+
// Next media player is already attached to this player and will start playing automatically
if (nextMediaPlayer != null) return
@@ -426,15 +452,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// We'll not receive new chunks anymore so we can stop the live listening
stop()
} else {
- // Enter in buffering mode and release current media player
- playingState = State.BUFFERING
- currentMediaPlayer?.release()
- currentMediaPlayer = null
+ playingState = State.Buffering
+ prepareNextMediaPlayer()
}
}
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
- stop()
+ Timber.d("## Voice Broadcast | onError: what=$what, extra=$extra")
+ // Do not change the playingState if the current player is still valid,
+ // the error will be thrown again when switching to the next player
+ if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) {
+ playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToPlay(what, extra))
+ }
return true
}
}
@@ -462,24 +491,25 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
val playbackTime = getCurrentPlaybackPosition()
val percentage = getCurrentPlaybackPercentage()
when (playingState) {
- State.PLAYING -> {
+ State.Playing -> {
if (playbackTime != null && percentage != null) {
playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage)
}
}
- State.PAUSED,
- State.BUFFERING -> {
+ State.Paused,
+ State.Buffering -> {
if (playbackTime != null && percentage != null) {
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
}
}
- State.IDLE -> {
+ State.Idle -> {
if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) {
playbackTracker.stopPlayback(id)
} else {
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
}
}
+ is State.Error -> Unit
}
}
}
diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_error.xml b/vector/src/main/res/drawable/ic_voice_broadcast_error.xml
new file mode 100644
index 0000000000..6cbd4592cb
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_voice_broadcast_error.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index 760293ee64..deec85e2ed 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -176,4 +176,27 @@
tools:ignore="NegativeMargin"
tools:text="-0:12" />
+
+
+
+