VoiceBroadcastPlayer - Extract some code to VoiceBroadcastPlaylist

This commit is contained in:
Florian Renaud 2022-11-04 17:12:02 +01:00
parent dae4162e75
commit c85b159952
3 changed files with 91 additions and 43 deletions

View File

@ -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

View File

@ -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<PlaylistItem>()
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<MessageAudioEvent>) {
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) {

View File

@ -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<PlaylistItem> = mutableListOf(),
) : List<PlaylistItem> 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<MessageAudioEvent>) {
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)