From c85b159952accaff1b7d65f027ced44fe790f0e4 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 4 Nov 2022 17:12:02 +0100 Subject: [PATCH] VoiceBroadcastPlayer - Extract some code to VoiceBroadcastPlaylist --- .../VoiceBroadcastExtensions.kt | 7 +- .../listening/VoiceBroadcastPlayerImpl.kt | 60 +++++------------ .../listening/VoiceBroadcastPlaylist.kt | 67 +++++++++++++++++++ 3 files changed, 91 insertions(+), 43 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt index a1328c0ba3..fa8033a211 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt @@ -16,9 +16,11 @@ package im.vector.app.features.voicebroadcast +import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -38,4 +40,7 @@ val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0 val VoiceBroadcastEvent.isLive - get() = content?.voiceBroadcastState != null && content?.voiceBroadcastState != VoiceBroadcastState.STOPPED + get() = content?.isLive.orFalse() + +val MessageVoiceBroadcastInfoContent.isLive + get() = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED 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 6eb9cbc735..de4f965a59 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 @@ -24,7 +24,6 @@ import im.vector.app.core.di.ActiveSessionHolder 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.duration 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 @@ -41,7 +40,6 @@ 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.session.room.model.message.MessageAudioContent -import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -67,11 +65,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var currentMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null - private var playlist = emptyList() - private var currentSequence: Int? = null + private val playlist = VoiceBroadcastPlaylist() private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null - private val isLive get() = currentVoiceBroadcastEvent?.isLive.orFalse() - private val lastSequence get() = currentVoiceBroadcastEvent?.content?.lastChunkSequence override var currentVoiceBroadcast: VoiceBroadcast? = null @@ -114,8 +109,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( voiceBroadcastStateTask = null // Clear playlist - playlist = emptyList() - currentSequence = null + playlist.reset() currentVoiceBroadcastEvent = null currentVoiceBroadcast = null @@ -152,25 +146,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) - .onEach(this::updatePlaylist) + .onEach { + playlist.setItems(it) + onPlaylistUpdated() + } .launchIn(sessionScope) } - private fun updatePlaylist(audioEvents: List) { - val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } - val chunkPositions = sorted - .map { it.duration } - .runningFold(0) { acc, i -> acc + i } - .dropLast(1) - playlist = sorted.mapIndexed { index, messageAudioEvent -> - PlaylistItem( - audioEvent = messageAudioEvent, - startTime = chunkPositions.getOrNull(index) ?: 0 - ) - } - onPlaylistUpdated() - } - private fun onPlaylistUpdated() { when (playingState) { State.PLAYING -> { @@ -201,18 +183,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( stopPlayer() val playlistItem = when { - position != null -> playlist.lastOrNull { it.startTime <= position } - isLive -> playlist.lastOrNull() + position != null -> playlist.findByPosition(position) + currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull() else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } - val sequence = playlistItem.audioEvent.sequence + val sequence = playlistItem.audioEvent.sequence ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return } val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0 sessionScope.launch { try { prepareMediaPlayer(content) { mp -> currentMediaPlayer = mp - currentSequence = sequence + playlist.currentSequence = sequence mp.start() if (sequencePosition > 0) { mp.seekTo(sequencePosition) @@ -241,10 +223,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun getNextAudioContent(): MessageAudioContent? { - val nextSequence = currentSequence?.plus(1) - ?: playlist.lastOrNull()?.audioEvent?.sequence - ?: 1 - return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content + return playlist.getNextItem()?.audioEvent?.content } private fun prepareNextMediaPlayer() { @@ -322,7 +301,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { when (what) { MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { - currentSequence = currentSequence?.plus(1) + playlist.currentSequence++ currentMediaPlayer = mp prepareNextMediaPlayer() } @@ -333,7 +312,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - if (!isLive && lastSequence == currentSequence) { + val content = currentVoiceBroadcastEvent?.content + val isLive = content?.isLive.orFalse() + if (!isLive && content?.lastChunkSequence == playlist.currentSequence) { // We'll not receive new chunks anymore so we can stop the live listening stop() } else { @@ -347,10 +328,6 @@ 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, ) { @@ -366,12 +343,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaybackTick(id: String) { if (currentMediaPlayer?.isPlaying.orFalse()) { - val itemStartPosition = currentSequence?.let { seq -> playlist.find { it.audioEvent.sequence == seq } }?.startTime + val itemStartPosition = playlist.currentItem?.startTime val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0) Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}") if (currentVoiceBroadcastPosition != null) { - val totalDuration = getVoiceBroadcastDuration() - val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration + val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage) } else { stopPlaybackTicker(id) @@ -385,7 +361,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTicker?.stop() playbackTicker = null - val totalDuration = getVoiceBroadcastDuration() + val totalDuration = playlist.duration val playbackTime = playbackTracker.getPlaybackTime(id) val remainingTime = totalDuration - playbackTime if (remainingTime < 1000) { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt new file mode 100644 index 0000000000..5cd6efc28a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 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.features.voicebroadcast.listening + +import im.vector.app.features.voicebroadcast.duration +import im.vector.app.features.voicebroadcast.sequence +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent + +class VoiceBroadcastPlaylist( + private val items: MutableList = mutableListOf(), +) : List by items { + + var currentSequence = 1 + val currentItem get() = findBySequence(currentSequence) + + val duration + get() = items.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0 + + fun setItems(audioEvents: List) { + items.clear() + val sorted = audioEvents.sortedBy { it.sequence?.toLong() ?: it.root.originServerTs } + val chunkPositions = sorted + .map { it.duration } + .runningFold(0) { acc, i -> acc + i } + .dropLast(1) + val newItems = sorted.mapIndexed { index, messageAudioEvent -> + PlaylistItem( + audioEvent = messageAudioEvent, + startTime = chunkPositions.getOrNull(index) ?: 0 + ) + } + items.addAll(newItems) + } + + fun reset() { + currentSequence = 1 + items.clear() + } + + fun findByPosition(positionMillis: Int): PlaylistItem? { + return items.lastOrNull { it.startTime <= positionMillis } + } + + fun findBySequence(sequenceNumber: Int): PlaylistItem? { + return items.find { it.audioEvent.sequence == sequenceNumber } + } + + fun getNextItem() = findBySequence(currentSequence.plus(1)) + + fun firstOrNull() = findBySequence(1) +} + +data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)