Fix pause/resume playback not working correctly

This commit is contained in:
Florian Renaud 2022-11-29 14:12:39 +01:00
parent 6bdf237cc9
commit 42b3ecc0b6
5 changed files with 74 additions and 39 deletions

View file

@ -0,0 +1,35 @@
/*
* Copyright 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.extensions
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
* Returns a flow that invokes the given action after the first value of the upstream flow is emitted downstream.
*/
fun <T> Flow<T>.onFirst(action: (T) -> Unit): Flow<T> = flow {
var emitted = false
collect { value ->
emit(value) // always emit value
if (!emitted) {
action(value) // execute the action after the first emission
emitted = true
}
}
}

View file

@ -149,7 +149,7 @@ class AudioMessageHelper @Inject constructor(
} }
private fun startPlayback(id: String, file: File) { private fun startPlayback(id: String, file: File) {
val currentPlaybackTime = playbackTracker.getPlaybackTime(id) val currentPlaybackTime = playbackTracker.getPlaybackTime(id) ?: 0
try { try {
FileInputStream(file).use { fis -> FileInputStream(file).use { fis ->

View file

@ -67,8 +67,8 @@ class AudioMessagePlaybackTracker @Inject constructor() {
} }
fun startPlayback(id: String) { fun startPlayback(id: String) {
val currentPlaybackTime = getPlaybackTime(id) val currentPlaybackTime = getPlaybackTime(id) ?: 0
val currentPercentage = getPercentage(id) val currentPercentage = getPercentage(id) ?: 0f
val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage) val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage)
setState(id, currentState) setState(id, currentState)
// Pause any active playback // Pause any active playback
@ -85,9 +85,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
} }
fun pausePlayback(id: String) { fun pausePlayback(id: String) {
if (getPlaybackState(id) is Listener.State.Playing) { val state = getPlaybackState(id)
val currentPlaybackTime = getPlaybackTime(id) if (state is Listener.State.Playing) {
val currentPercentage = getPercentage(id) val currentPlaybackTime = state.playbackTime
val currentPercentage = state.percentage
setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage)) setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage))
} }
} }
@ -110,21 +111,23 @@ class AudioMessagePlaybackTracker @Inject constructor() {
fun getPlaybackState(id: String) = states[id] fun getPlaybackState(id: String) = states[id]
fun getPlaybackTime(id: String): Int { fun getPlaybackTime(id: String): Int? {
return when (val state = states[id]) { return when (val state = states[id]) {
is Listener.State.Playing -> state.playbackTime is Listener.State.Playing -> state.playbackTime
is Listener.State.Paused -> state.playbackTime is Listener.State.Paused -> state.playbackTime
/* Listener.State.Idle, */ is Listener.State.Recording,
else -> 0 Listener.State.Idle,
null -> null
} }
} }
fun getPercentage(id: String): Float { fun getPercentage(id: String): Float? {
return when (val state = states[id]) { return when (val state = states[id]) {
is Listener.State.Playing -> state.percentage is Listener.State.Playing -> state.percentage
is Listener.State.Paused -> state.percentage is Listener.State.Paused -> state.percentage
/* Listener.State.Idle, */ is Listener.State.Recording,
else -> 0f Listener.State.Idle,
null -> null
} }
} }

View file

@ -141,14 +141,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
renderBackwardForwardButtons(holder, playbackState) renderBackwardForwardButtons(holder, playbackState)
renderLiveIndicator(holder) renderLiveIndicator(holder)
if (!isUserSeeking) { if (!isUserSeeking) {
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
} }
} }
} }
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) { private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused
val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
val canBackward = isPlayingOrPaused && playbackTime > 0 val canBackward = isPlayingOrPaused && playbackTime > 0
val canForward = isPlayingOrPaused && playbackTime < duration val canForward = isPlayingOrPaused && playbackTime < duration
holder.fastBackwardButton.isInvisible = !canBackward holder.fastBackwardButton.isInvisible = !canBackward

View file

@ -21,6 +21,7 @@ import android.media.MediaPlayer
import android.media.MediaPlayer.OnPreparedListener import android.media.MediaPlayer.OnPreparedListener
import androidx.annotation.MainThread import androidx.annotation.MainThread
import im.vector.app.core.di.ActiveSessionHolder 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.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voice.VoiceFailure
@ -145,11 +146,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
playingState = State.BUFFERING playingState = State.BUFFERING
observeVoiceBroadcastStateEvent(voiceBroadcast) observeVoiceBroadcastStateEvent(voiceBroadcast)
fetchPlaylistAndStartPlayback(voiceBroadcast)
} }
private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) { private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) {
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
.onFirst { fetchPlaylistAndStartPlayback(voiceBroadcast) }
.onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) } .onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) }
.launchIn(sessionScope) .launchIn(sessionScope)
} }
@ -222,24 +223,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
} }
} }
private fun pausePlayback(positionMillis: Int? = null) { private fun pausePlayback() {
if (positionMillis == null) { playingState = State.PAUSED // This will trigger a playing state update and save the current position
if (currentMediaPlayer != null) {
currentMediaPlayer?.pause() currentMediaPlayer?.pause()
} else { } else {
stopPlayer() stopPlayer()
val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId
val duration = playlist.duration.takeIf { it > 0 }
if (voiceBroadcastId != null && duration != null) {
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
} }
} }
playingState = State.PAUSED
}
private fun resumePlayback() { private fun resumePlayback() {
if (currentMediaPlayer != null) { if (currentMediaPlayer != null) {
currentMediaPlayer?.start()
playingState = State.PLAYING playingState = State.PLAYING
currentMediaPlayer?.start()
} else { } else {
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0 val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
startPlayback(savedPosition) startPlayback(savedPosition)
@ -256,7 +252,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
startPlayback(positionMillis) startPlayback(positionMillis)
} }
playingState == State.IDLE || playingState == State.PAUSED -> { playingState == State.IDLE || playingState == State.PAUSED -> {
pausePlayback(positionMillis) stopPlayer()
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
} }
} }
} }
@ -366,8 +363,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
isLiveListening && newSequence == playlist.currentSequence isLiveListening && newSequence == playlist.currentSequence
} }
} }
// otherwise, stay in live or go in live if we reached the latest sequence // if there is no saved position, go in live
else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence getCurrentPlaybackPosition() == null -> true
// if we reached the latest sequence, go in live
playlist.currentSequence == playlist.lastOrNull()?.sequence -> true
// otherwise, do not change
else -> isLiveListening
} }
} }
@ -392,9 +393,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
} }
private fun getCurrentPlaybackPosition(): Int? { private fun getCurrentPlaybackPosition(): Int? {
val playlistPosition = playlist.currentItem?.startTime val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) }
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId)
return computedPosition ?: savedPosition return computedPosition ?: savedPosition
} }
@ -423,19 +424,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// Next media player is already attached to this player and will start playing automatically // Next media player is already attached to this player and will start playing automatically
if (nextMediaPlayer != null) return if (nextMediaPlayer != null) return
// Next media player is preparing but not attached yet, reset the currentMediaPlayer and let the new player take over val hasEnded = !isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence
if (isPreparingNextPlayer) { if (hasEnded) {
currentMediaPlayer?.release()
currentMediaPlayer = null
playingState = State.BUFFERING
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()
} else { } else {
// Enter in buffering mode and release current media player
playingState = State.BUFFERING playingState = State.BUFFERING
currentMediaPlayer?.release()
currentMediaPlayer = null
} }
} }