VoiceBroadcastPlayer - Extract some code to VoiceBroadcastPlaylist
This commit is contained in:
parent
dae4162e75
commit
c85b159952
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue