mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 09:55:40 +03:00
Handle playback error
This commit is contained in:
parent
f8852856c6
commit
2d24eb1273
15 changed files with 144 additions and 43 deletions
1
changelog.d/7829.bugfix
Normal file
1
changelog.d/7829.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Handle exceptions when listening a voice broadcast
|
|
@ -3121,6 +3121,7 @@
|
|||
<string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
|
||||
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
|
||||
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
|
||||
<string name="error_voice_broadcast_unable_to_play">Unable to play this voice broadcast.</string>
|
||||
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
|
||||
<string name="voice_broadcast_recording_time_left">%1$s left</string>
|
||||
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
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
|
||||
|
@ -45,6 +46,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,
|
||||
|
@ -82,6 +84,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
||||
colorProvider = colorProvider,
|
||||
drawableProvider = drawableProvider,
|
||||
errorFormatter = errorFormatter,
|
||||
)
|
||||
|
||||
return if (isRecording) {
|
||||
|
|
|
@ -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<Int>) : State()
|
||||
|
|
|
@ -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<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
||||
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
||||
protected val avatarRenderer get() = attributes.avatarRenderer
|
||||
protected val errorFormatter get() = voiceBroadcastAttributes.errorFormatter
|
||||
protected val callback get() = attributes.callback
|
||||
|
||||
override fun isCacheable(): Boolean = false
|
||||
|
@ -107,5 +109,6 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
val roomItem: MatrixItem?,
|
||||
val colorProvider: ColorProvider,
|
||||
val drawableProvider: DrawableProvider,
|
||||
val errorFormatter: ErrorFormatter,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
|||
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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
@ -66,6 +78,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
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 {
|
||||
|
@ -109,6 +122,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
||||
}
|
||||
is VoiceBroadcastPlayer.State.Error,
|
||||
VoiceBroadcastPlayer.State.Idle,
|
||||
VoiceBroadcastPlayer.State.Paused -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
|
@ -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<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
|
||||
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
|
||||
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
|
||||
val errorView by bind<TextView>(R.id.errorView)
|
||||
val controlsGroup by bind<Group>(R.id.controlsGroup)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -124,6 +124,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
|
||||
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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -72,6 +73,7 @@ interface VoiceBroadcastPlayer {
|
|||
object Playing : State
|
||||
object Paused : State
|
||||
object Buffering : State
|
||||
data class Error(val failure: VoiceBroadcastFailure.ListeningError) : State
|
||||
object Idle : State
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -193,6 +193,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
startPlayback(nextItem.startTime)
|
||||
}
|
||||
}
|
||||
is State.Error -> Unit
|
||||
State.Idle -> Unit // Should not happen
|
||||
}
|
||||
}
|
||||
|
@ -205,20 +206,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
val sequence = playlistItem.sequence ?: run { Timber.w("## Voice Broadcast | Playlist item has no sequence"); return }
|
||||
val sequencePosition = position - playlistItem.startTime
|
||||
sessionScope.launch {
|
||||
try {
|
||||
prepareMediaPlayer(content) { mp ->
|
||||
currentMediaPlayer = mp
|
||||
playlist.currentSequence = sequence
|
||||
mp.start()
|
||||
if (sequencePosition > 0) {
|
||||
mp.seekTo(sequencePosition)
|
||||
}
|
||||
playingState = State.Playing
|
||||
prepareNextMediaPlayer()
|
||||
prepareMediaPlayer(content) { mp ->
|
||||
currentMediaPlayer = mp
|
||||
playlist.currentSequence = sequence
|
||||
mp.start()
|
||||
if (sequencePosition > 0) {
|
||||
mp.seekTo(sequencePosition)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## Voice Broadcast | Unable to start playback: $failure")
|
||||
throw VoiceFailure.UnableToPlay(failure)
|
||||
playingState = State.Playing
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -275,6 +271,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
mp.start()
|
||||
onNextMediaPlayerStarted(mp)
|
||||
}
|
||||
is State.Error,
|
||||
State.Idle -> stopPlayer()
|
||||
}
|
||||
}
|
||||
|
@ -288,11 +285,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 +300,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
)
|
||||
setDataSource(fis.fd)
|
||||
setOnInfoListener(mediaPlayerListener)
|
||||
setOnErrorListener(mediaPlayerListener)
|
||||
setOnPreparedListener(onPreparedListener)
|
||||
setOnCompletionListener(mediaPlayerListener)
|
||||
prepare()
|
||||
prepareAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -330,8 +327,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
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) }
|
||||
}
|
||||
|
@ -374,7 +378,8 @@ 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()
|
||||
}
|
||||
}
|
||||
|
@ -389,16 +394,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 +421,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCompletion(mp: MediaPlayer) {
|
||||
// Release media player as soon as it completed
|
||||
mp.release()
|
||||
currentMediaPlayer = null
|
||||
|
||||
// Next media player is already attached to this player and will start playing automatically
|
||||
if (nextMediaPlayer != null) return
|
||||
|
||||
|
@ -426,15 +435,16 @@ 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
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||
stop()
|
||||
Timber.d("## Voice Broadcast | onError: what=$what, extra=$extra")
|
||||
if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) {
|
||||
playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToPlay(what, extra))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -480,6 +490,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||
}
|
||||
}
|
||||
is State.Error -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
vector/src/main/res/drawable/ic_voice_broadcast_error.xml
Normal file
10
vector/src/main/res/drawable/ic_voice_broadcast_error.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M8,16C12.418,16 16,12.418 16,8C16,3.582 12.418,0 8,0C3.582,0 0,3.582 0,8C0,12.418 3.582,16 8,16ZM6.777,4.135C6.717,3.451 7.221,2.851 7.905,2.803C8.577,2.755 9.177,3.259 9.249,3.943V4.135L8.865,8.935C8.829,9.379 8.457,9.715 8.013,9.715H7.941C7.521,9.679 7.197,9.355 7.161,8.935L6.777,4.135ZM9.056,12.067C9.056,12.651 8.583,13.123 8,13.123C7.417,13.123 6.944,12.651 6.944,12.067C6.944,11.484 7.417,11.011 8,11.011C8.583,11.011 9.056,11.484 9.056,12.067Z"
|
||||
android:fillColor="#FF5B55"
|
||||
android:fillType="evenOdd" />
|
||||
</vector>
|
|
@ -176,4 +176,27 @@
|
|||
tools:ignore="NegativeMargin"
|
||||
tools:text="-0:12" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/controlsGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="controllerButtonsFlow,seekBar,elapsedTime,remainingTime" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorView"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@string/error_voice_broadcast_unable_to_play"
|
||||
android:textColor="?colorError"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_voice_broadcast_error"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
Loading…
Reference in a new issue