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
|
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.VoiceBroadcastChunk
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
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.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
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 MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0
|
||||||
|
|
||||||
val VoiceBroadcastEvent.isLive
|
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.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.voice.VoiceFailure
|
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.isLive
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
|
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
|
||||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
|
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.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
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.MessageAudioContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -67,11 +65,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
private var currentMediaPlayer: MediaPlayer? = null
|
private var currentMediaPlayer: MediaPlayer? = null
|
||||||
private var nextMediaPlayer: MediaPlayer? = null
|
private var nextMediaPlayer: MediaPlayer? = null
|
||||||
|
|
||||||
private var playlist = emptyList<PlaylistItem>()
|
private val playlist = VoiceBroadcastPlaylist()
|
||||||
private var currentSequence: Int? = null
|
|
||||||
private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
|
private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
|
||||||
private val isLive get() = currentVoiceBroadcastEvent?.isLive.orFalse()
|
|
||||||
private val lastSequence get() = currentVoiceBroadcastEvent?.content?.lastChunkSequence
|
|
||||||
|
|
||||||
override var currentVoiceBroadcast: VoiceBroadcast? = null
|
override var currentVoiceBroadcast: VoiceBroadcast? = null
|
||||||
|
|
||||||
@ -114,8 +109,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
voiceBroadcastStateTask = null
|
voiceBroadcastStateTask = null
|
||||||
|
|
||||||
// Clear playlist
|
// Clear playlist
|
||||||
playlist = emptyList()
|
playlist.reset()
|
||||||
currentSequence = null
|
|
||||||
|
|
||||||
currentVoiceBroadcastEvent = null
|
currentVoiceBroadcastEvent = null
|
||||||
currentVoiceBroadcast = null
|
currentVoiceBroadcast = null
|
||||||
@ -152,25 +146,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
|
|
||||||
private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
|
private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||||
fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
|
fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
|
||||||
.onEach(this::updatePlaylist)
|
.onEach {
|
||||||
|
playlist.setItems(it)
|
||||||
|
onPlaylistUpdated()
|
||||||
|
}
|
||||||
.launchIn(sessionScope)
|
.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() {
|
private fun onPlaylistUpdated() {
|
||||||
when (playingState) {
|
when (playingState) {
|
||||||
State.PLAYING -> {
|
State.PLAYING -> {
|
||||||
@ -201,18 +183,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
stopPlayer()
|
stopPlayer()
|
||||||
|
|
||||||
val playlistItem = when {
|
val playlistItem = when {
|
||||||
position != null -> playlist.lastOrNull { it.startTime <= position }
|
position != null -> playlist.findByPosition(position)
|
||||||
isLive -> playlist.lastOrNull()
|
currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
|
||||||
else -> playlist.firstOrNull()
|
else -> playlist.firstOrNull()
|
||||||
}
|
}
|
||||||
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
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
|
val sequencePosition = position?.let { it - playlistItem.startTime } ?: 0
|
||||||
sessionScope.launch {
|
sessionScope.launch {
|
||||||
try {
|
try {
|
||||||
prepareMediaPlayer(content) { mp ->
|
prepareMediaPlayer(content) { mp ->
|
||||||
currentMediaPlayer = mp
|
currentMediaPlayer = mp
|
||||||
currentSequence = sequence
|
playlist.currentSequence = sequence
|
||||||
mp.start()
|
mp.start()
|
||||||
if (sequencePosition > 0) {
|
if (sequencePosition > 0) {
|
||||||
mp.seekTo(sequencePosition)
|
mp.seekTo(sequencePosition)
|
||||||
@ -241,10 +223,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getNextAudioContent(): MessageAudioContent? {
|
private fun getNextAudioContent(): MessageAudioContent? {
|
||||||
val nextSequence = currentSequence?.plus(1)
|
return playlist.getNextItem()?.audioEvent?.content
|
||||||
?: playlist.lastOrNull()?.audioEvent?.sequence
|
|
||||||
?: 1
|
|
||||||
return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun prepareNextMediaPlayer() {
|
private fun prepareNextMediaPlayer() {
|
||||||
@ -322,7 +301,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||||
when (what) {
|
when (what) {
|
||||||
MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
|
MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
|
||||||
currentSequence = currentSequence?.plus(1)
|
playlist.currentSequence++
|
||||||
currentMediaPlayer = mp
|
currentMediaPlayer = mp
|
||||||
prepareNextMediaPlayer()
|
prepareNextMediaPlayer()
|
||||||
}
|
}
|
||||||
@ -333,7 +312,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
override fun onCompletion(mp: MediaPlayer) {
|
override fun onCompletion(mp: MediaPlayer) {
|
||||||
if (nextMediaPlayer != null) return
|
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
|
// We'll not receive new chunks anymore so we can stop the live listening
|
||||||
stop()
|
stop()
|
||||||
} else {
|
} 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 inner class PlaybackTicker(
|
||||||
private var playbackTicker: CountUpTimer? = null,
|
private var playbackTicker: CountUpTimer? = null,
|
||||||
) {
|
) {
|
||||||
@ -366,12 +343,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
|
|
||||||
private fun onPlaybackTick(id: String) {
|
private fun onPlaybackTick(id: String) {
|
||||||
if (currentMediaPlayer?.isPlaying.orFalse()) {
|
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)
|
val currentVoiceBroadcastPosition = itemStartPosition?.plus(currentMediaPlayer?.currentPosition ?: 0)
|
||||||
Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}")
|
Timber.d("Voice Broadcast | VoiceBroadcastPlayerImpl - sequence: $currentSequence, itemStartPosition $itemStartPosition, currentMediaPlayer=$currentMediaPlayer, currentMediaPlayer?.currentPosition: ${currentMediaPlayer?.currentPosition}")
|
||||||
if (currentVoiceBroadcastPosition != null) {
|
if (currentVoiceBroadcastPosition != null) {
|
||||||
val totalDuration = getVoiceBroadcastDuration()
|
val percentage = currentVoiceBroadcastPosition.toFloat() / playlist.duration
|
||||||
val percentage = currentVoiceBroadcastPosition.toFloat() / totalDuration
|
|
||||||
playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage)
|
playbackTracker.updatePlayingAtPlaybackTime(id, currentVoiceBroadcastPosition, percentage)
|
||||||
} else {
|
} else {
|
||||||
stopPlaybackTicker(id)
|
stopPlaybackTicker(id)
|
||||||
@ -385,7 +361,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||||||
playbackTicker?.stop()
|
playbackTicker?.stop()
|
||||||
playbackTicker = null
|
playbackTicker = null
|
||||||
|
|
||||||
val totalDuration = getVoiceBroadcastDuration()
|
val totalDuration = playlist.duration
|
||||||
val playbackTime = playbackTracker.getPlaybackTime(id)
|
val playbackTime = playbackTracker.getPlaybackTime(id)
|
||||||
val remainingTime = totalDuration - playbackTime
|
val remainingTime = totalDuration - playbackTime
|
||||||
if (remainingTime < 1000) {
|
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…
x
Reference in New Issue
Block a user