Merge pull request #7646 from vector-im/bugfix/fre/fix_playback_stuck_in_buffering

Voice Broadcast - Fix playback stuck in buffering
This commit is contained in:
Florian Renaud 2022-11-29 09:59:08 +01:00 committed by GitHub
commit 559af32ab6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 57 additions and 39 deletions

1
changelog.d/7646.bugfix Normal file
View file

@ -0,0 +1 @@
Voice Broadcast - Fix playback stuck in buffering mode

View file

@ -36,7 +36,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import timber.log.Timber import timber.log.Timber
@ -73,7 +72,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
@MainThread @MainThread
set(value) { set(value) {
if (field != value) { if (field != value) {
Timber.w("isLiveListening: $field -> $value") Timber.d("## Voice Broadcast | isLiveListening: $field -> $value")
field = value field = value
onLiveListeningChanged(value) onLiveListeningChanged(value)
} }
@ -83,7 +82,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
@MainThread @MainThread
set(value) { set(value) {
if (field != value) { if (field != value) {
Timber.w("playingState: $field -> $value") Timber.d("## Voice Broadcast | playingState: $field -> $value")
field = value field = value
onPlayingStateChanged(value) onPlayingStateChanged(value)
} }
@ -175,41 +174,35 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun onPlaylistUpdated() { private fun onPlaylistUpdated() {
when (playingState) { when (playingState) {
State.PLAYING -> { State.PLAYING,
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
prepareNextMediaPlayer()
}
}
State.PAUSED -> { State.PAUSED -> {
if (nextMediaPlayer == null && !isPreparingNextPlayer) { if (nextMediaPlayer == null && !isPreparingNextPlayer) {
prepareNextMediaPlayer() prepareNextMediaPlayer()
} }
} }
State.BUFFERING -> { State.BUFFERING -> {
val nextItem = playlist.getNextItem() val nextItem = if (isLiveListening && playlist.currentSequence == null) {
// live listening, jump to the last item if playback has not started
playlist.lastOrNull()
} else {
// not live or playback already started, request next item
playlist.getNextItem()
}
if (nextItem != null) { if (nextItem != null) {
val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) } startPlayback(nextItem.startTime)
startPlayback(savedPosition?.takeIf { it > 0 })
} }
} }
State.IDLE -> { State.IDLE -> Unit // Should not happen
val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
startPlayback(savedPosition?.takeIf { it > 0 })
}
} }
} }
private fun startPlayback(position: Int? = null) { private fun startPlayback(position: Int) {
stopPlayer() stopPlayer()
val playlistItem = when { val playlistItem = playlist.findByPosition(position)
position != null -> playlist.findByPosition(position) val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## Voice Broadcast | No content to play at position $position"); return }
mostRecentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull() val sequence = playlistItem.sequence ?: run { Timber.w("## Voice Broadcast | Playlist item has no sequence"); return }
else -> playlist.firstOrNull() val sequencePosition = position - playlistItem.startTime
}
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
val sequence = playlistItem.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return }
val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0
sessionScope.launch { sessionScope.launch {
try { try {
prepareMediaPlayer(content) { mp -> prepareMediaPlayer(content) { mp ->
@ -223,7 +216,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
prepareNextMediaPlayer() prepareNextMediaPlayer()
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "Unable to start playback") Timber.e(failure, "## Voice Broadcast | Unable to start playback: $failure")
throw VoiceFailure.UnableToPlay(failure) throw VoiceFailure.UnableToPlay(failure)
} }
} }
@ -248,8 +241,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
currentMediaPlayer?.start() currentMediaPlayer?.start()
playingState = State.PLAYING playingState = State.PLAYING
} else { } else {
val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
startPlayback(position) startPlayback(savedPosition)
} }
} }
@ -274,9 +267,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
isPreparingNextPlayer = true isPreparingNextPlayer = true
sessionScope.launch { sessionScope.launch {
prepareMediaPlayer(nextItem.audioEvent.content) { mp -> prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
nextMediaPlayer = mp
currentMediaPlayer?.setNextMediaPlayer(mp)
isPreparingNextPlayer = false isPreparingNextPlayer = false
nextMediaPlayer = mp
when (playingState) {
State.PLAYING,
State.PAUSED -> {
currentMediaPlayer?.setNextMediaPlayer(mp)
}
State.BUFFERING -> {
mp.start()
onNextMediaPlayerStarted(mp)
}
State.IDLE -> stopPlayer()
}
} }
} }
} }
@ -287,7 +290,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
val audioFile = try { val audioFile = try {
session.fileService().downloadFile(messageAudioContent) session.fileService().downloadFile(messageAudioContent)
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "Unable to start playback") Timber.e(failure, "Voice Broadcast | Download has failed: $failure")
throw VoiceFailure.UnableToPlay(failure) throw VoiceFailure.UnableToPlay(failure)
} }
@ -373,6 +376,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// Notify live mode change to all the listeners attached to the current voice broadcast id // Notify live mode change to all the listeners attached to the current voice broadcast id
listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) } listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) }
} }
// Live has ended and last chunk has been reached, we can stop the playback
if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) {
stop()
}
}
private fun onNextMediaPlayerStarted(mp: MediaPlayer) {
playingState = State.PLAYING
playlist.currentSequence = playlist.currentSequence?.inc()
currentMediaPlayer = mp
nextMediaPlayer = null
prepareNextMediaPlayer()
} }
private fun getCurrentPlaybackPosition(): Int? { private fun getCurrentPlaybackPosition(): Int? {
@ -398,23 +414,24 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
when (what) { when (what) {
MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> onNextMediaPlayerStarted(mp)
playlist.currentSequence = playlist.currentSequence?.inc()
currentMediaPlayer = mp
nextMediaPlayer = null
playingState = State.PLAYING
prepareNextMediaPlayer()
}
} }
return false return false
} }
override fun onCompletion(mp: MediaPlayer) { override fun onCompletion(mp: MediaPlayer) {
// Next media player is already attached to this player and will start playing automatically
if (nextMediaPlayer != null) return if (nextMediaPlayer != null) return
if (isLiveListening || mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) { // Next media player is preparing but not attached yet, reset the currentMediaPlayer and let the new player take over
if (isPreparingNextPlayer) {
currentMediaPlayer?.release()
currentMediaPlayer = null
playingState = State.BUFFERING playingState = State.BUFFERING
} else { return
}
if (!isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
// We'll not receive new chunks anymore so we can stop the live listening // We'll not receive new chunks anymore so we can stop the live listening
stop() stop()
} }