diff --git a/vector/src/main/java/im/vector/app/core/extensions/Flow.kt b/vector/src/main/java/im/vector/app/core/extensions/Flow.kt new file mode 100644 index 0000000000..82e6e5f9a6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/Flow.kt @@ -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 Flow.onFirst(action: (T) -> Unit): Flow = flow { + var emitted = false + collect { value -> + emit(value) // always emit value + + if (!emitted) { + action(value) // execute the action after the first emission + emitted = true + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt index b5ea528bd7..900de041d0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt @@ -149,7 +149,7 @@ class AudioMessageHelper @Inject constructor( } private fun startPlayback(id: String, file: File) { - val currentPlaybackTime = playbackTracker.getPlaybackTime(id) + val currentPlaybackTime = playbackTracker.getPlaybackTime(id) ?: 0 try { FileInputStream(file).use { fis -> 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 90fd66f9ab..c34cbbc74a 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 @@ -67,8 +67,8 @@ class AudioMessagePlaybackTracker @Inject constructor() { } fun startPlayback(id: String) { - val currentPlaybackTime = getPlaybackTime(id) - val currentPercentage = getPercentage(id) + val currentPlaybackTime = getPlaybackTime(id) ?: 0 + val currentPercentage = getPercentage(id) ?: 0f val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage) setState(id, currentState) // Pause any active playback @@ -85,9 +85,10 @@ class AudioMessagePlaybackTracker @Inject constructor() { } fun pausePlayback(id: String) { - if (getPlaybackState(id) is Listener.State.Playing) { - val currentPlaybackTime = getPlaybackTime(id) - val currentPercentage = getPercentage(id) + val state = getPlaybackState(id) + if (state is Listener.State.Playing) { + val currentPlaybackTime = state.playbackTime + val currentPercentage = state.percentage setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage)) } } @@ -110,21 +111,23 @@ class AudioMessagePlaybackTracker @Inject constructor() { fun getPlaybackState(id: String) = states[id] - fun getPlaybackTime(id: String): Int { + fun getPlaybackTime(id: String): Int? { return when (val state = states[id]) { is Listener.State.Playing -> state.playbackTime is Listener.State.Paused -> state.playbackTime - /* Listener.State.Idle, */ - else -> 0 + is Listener.State.Recording, + Listener.State.Idle, + null -> null } } - fun getPercentage(id: String): Float { + fun getPercentage(id: String): Float? { return when (val state = states[id]) { is Listener.State.Playing -> state.percentage is Listener.State.Paused -> state.percentage - /* Listener.State.Idle, */ - else -> 0f + is Listener.State.Recording, + Listener.State.Idle, + null -> null } } 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 8d32875f0c..38fe1e8f17 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 @@ -141,14 +141,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem renderBackwardForwardButtons(holder, playbackState) renderLiveIndicator(holder) if (!isUserSeeking) { - holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) + holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0 } } } private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) { 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 canForward = isPlayingOrPaused && playbackTime < duration holder.fastBackwardButton.isInvisible = !canBackward 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 95a4ddcf2e..f8025d078e 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 @@ -21,6 +21,7 @@ import android.media.MediaPlayer import android.media.MediaPlayer.OnPreparedListener import androidx.annotation.MainThread 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 @@ -145,11 +146,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.BUFFERING observeVoiceBroadcastStateEvent(voiceBroadcast) - fetchPlaylistAndStartPlayback(voiceBroadcast) } private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) { voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + .onFirst { fetchPlaylistAndStartPlayback(voiceBroadcast) } .onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) } .launchIn(sessionScope) } @@ -222,24 +223,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - private fun pausePlayback(positionMillis: Int? = null) { - if (positionMillis == null) { + private fun pausePlayback() { + playingState = State.PAUSED // This will trigger a playing state update and save the current position + if (currentMediaPlayer != null) { currentMediaPlayer?.pause() } else { 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() { if (currentMediaPlayer != null) { - currentMediaPlayer?.start() playingState = State.PLAYING + currentMediaPlayer?.start() } else { val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0 startPlayback(savedPosition) @@ -256,7 +252,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( startPlayback(positionMillis) } 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 } } - // otherwise, stay in live or go in live if we reached the latest sequence - else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence + // if there is no saved position, go in live + 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? { - val playlistPosition = playlist.currentItem?.startTime - val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition - val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } + val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null + val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) } + val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId) 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 if (nextMediaPlayer != null) return - // 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 - return - } - - if (!isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) { + val hasEnded = !isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence + if (hasEnded) { // 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 } }