From b0a31304a1ae0649c478884996013c70718486dc Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 31 Oct 2022 17:04:49 +0100 Subject: [PATCH] Update seek bar tick progress while playing --- .../factory/VoiceBroadcastItemFactory.kt | 3 + .../item/AbsMessageVoiceBroadcastItem.kt | 4 + .../MessageVoiceBroadcastListeningItem.kt | 30 +++++++- .../listening/VoiceBroadcastPlayerImpl.kt | 74 +++++++++++++++++-- 4 files changed, 100 insertions(+), 11 deletions(-) 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 5d9c663210..06d3563303 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 @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory 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 import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem @@ -44,6 +45,7 @@ class VoiceBroadcastItemFactory @Inject constructor( private val drawableProvider: DrawableProvider, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, + private val playbackTracker: AudioMessagePlaybackTracker, ) { fun create( @@ -71,6 +73,7 @@ class VoiceBroadcastItemFactory @Inject constructor( recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, + playbackTracker = playbackTracker, roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(), colorProvider = colorProvider, drawableProvider = drawableProvider, 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 7ada0c71f2..9ea0a634c5 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 @@ -25,6 +25,7 @@ import im.vector.app.R import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder @@ -40,6 +41,8 @@ abstract class AbsMessageVoiceBroadcastItem() { private lateinit var playerListener: VoiceBroadcastPlayer.Listener + private var isUserSeeking = false override fun bind(holder: Holder) { super.bind(holder) @@ -86,15 +88,36 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun bindSeekBar(holder: Holder) { - holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration) - holder.seekBar.max = voiceBroadcastAttributes.duration + holder.durationView.text = formatPlaybackTime(duration) + holder.seekBar.max = duration holder.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit - override fun onStartTrackingTouch(seekBar: SeekBar) = Unit + override fun onStartTrackingTouch(seekBar: SeekBar) { + isUserSeeking = true + } override fun onStopTrackingTouch(seekBar: SeekBar) { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress)) + isUserSeeking = false + } + }) + playbackTracker.track(voiceBroadcastId, object : AudioMessagePlaybackTracker.Listener { + override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Paused -> { + if (!isUserSeeking) { + holder.seekBar.progress = state.playbackTime + } + } + is AudioMessagePlaybackTracker.Listener.State.Playing -> { + if (!isUserSeeking) { + holder.seekBar.progress = state.playbackTime + } + } + AudioMessagePlaybackTracker.Listener.State.Idle -> Unit + is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit + } } }) } @@ -105,6 +128,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem super.unbind(holder) player.removeListener(voiceBroadcastId, playerListener) holder.seekBar.setOnSeekBarChangeListener(null) + playbackTracker.untrack(voiceBroadcastId) } override fun getViewStubId() = STUB_ID 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 166e5a12e5..4fbaee8374 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 @@ -29,6 +29,7 @@ import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroad import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.sequence import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase +import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.orFalse 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.MessageAudioEvent @@ -60,6 +62,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var voiceBroadcastStateJob: Job? = null private val mediaPlayerListener = MediaPlayerListener() + private val playbackTicker = PlaybackTicker() private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null @@ -79,6 +82,24 @@ class VoiceBroadcastPlayerImpl @Inject constructor( field = value // Notify state change to all the listeners attached to the current voice broadcast id currentVoiceBroadcastId?.let { voiceBroadcastId -> + when (value) { + State.PLAYING -> { + playbackTracker.startPlayback(voiceBroadcastId) + playbackTicker.startPlaybackTicker(voiceBroadcastId) + } + State.PAUSED -> { + playbackTracker.pausePlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + State.BUFFERING -> { + playbackTracker.pausePlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + State.IDLE -> { + playbackTracker.stopPlayback(voiceBroadcastId) + playbackTicker.stopPlaybackTicker() + } + } listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) } } } @@ -99,15 +120,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun pause() { - currentMediaPlayer?.pause() - currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) } playingState = State.PAUSED + currentMediaPlayer?.pause() } override fun stop() { + // Update state + playingState = State.IDLE + // Stop playback currentMediaPlayer?.stop() - currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } isLive = false // Release current player @@ -126,9 +148,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( fetchPlaylistJob?.cancel() fetchPlaylistJob = null - // Update state - playingState = State.IDLE - // Clear playlist playlist = emptyList() currentSequence = null @@ -218,7 +237,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (position > 0) { currentMediaPlayer?.seekTo(position) } - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } currentSequence = computedSequence withContext(Dispatchers.Main) { playingState = State.PLAYING } nextMediaPlayer = prepareNextMediaPlayer() @@ -231,7 +249,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun resumePlayback() { currentMediaPlayer?.start() - currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } playingState = State.PLAYING } @@ -352,4 +369,45 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) + + private inner class PlaybackTicker( + private var playbackTicker: CountUpTimer? = null, + ) { + + fun startPlaybackTicker(id: String) { + playbackTicker?.stop() + playbackTicker = CountUpTimer().apply { + tickListener = object : CountUpTimer.TickListener { + override fun onTick(milliseconds: Long) { + onPlaybackTick(id) + } + } + resume() + } + onPlaybackTick(id) + } + + private fun onPlaybackTick(id: String) { + if (currentMediaPlayer?.isPlaying.orFalse()) { + val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime + val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) + if (currentVoiceBroadcastPosition != null) { + val totalDuration = getVoiceBroadcastDuration() + val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration + playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) + } else { + playbackTracker.stopPlayback(id) + stopPlaybackTicker() + } + } else { + playbackTracker.stopPlayback(id) + stopPlaybackTicker() + } + } + + fun stopPlaybackTicker() { + playbackTicker?.stop() + playbackTicker = null + } + } }