mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 03:48:12 +03:00
VoiceBroadcastPlayer - seek implementation
This commit is contained in:
parent
a851e5aa85
commit
9219043579
10 changed files with 107 additions and 27 deletions
|
@ -129,9 +129,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
}
|
||||
|
||||
sealed class Listening : VoiceBroadcastAction() {
|
||||
data class PlayOrResume(val eventId: String) : Listening()
|
||||
data class PlayOrResume(val voiceBroadcastId: String) : Listening()
|
||||
object Pause : Listening()
|
||||
object Stop : Listening()
|
||||
data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
|
|||
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||
import im.vector.app.features.home.room.detail.error.RoomNotFound
|
||||
import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase
|
||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||
|
@ -478,7 +479,7 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
|
||||
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
|
||||
is RoomDetailAction.VoiceBroadcastAction -> handleVoiceBroadcastAction(action)
|
||||
is VoiceBroadcastAction -> handleVoiceBroadcastAction(action)
|
||||
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
|
||||
|
@ -620,22 +621,23 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleVoiceBroadcastAction(action: RoomDetailAction.VoiceBroadcastAction) {
|
||||
private fun handleVoiceBroadcastAction(action: VoiceBroadcastAction) {
|
||||
if (room == null) return
|
||||
viewModelScope.launch {
|
||||
when (action) {
|
||||
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> {
|
||||
VoiceBroadcastAction.Recording.Start -> {
|
||||
voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold(
|
||||
{ _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) },
|
||||
{ _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) },
|
||||
)
|
||||
}
|
||||
RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
||||
RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
||||
RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
||||
is RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.eventId)
|
||||
RoomDetailAction.VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
|
||||
RoomDetailAction.VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
|
||||
VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
||||
VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
||||
VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
||||
is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.voiceBroadcastId)
|
||||
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
|
||||
VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
|
||||
is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
|
||||
voiceBroadcastId = voiceBroadcastId,
|
||||
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
||||
duration = voiceBroadcastEventsGroup.getDuration(),
|
||||
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
||||
recorder = voiceBroadcastRecorder,
|
||||
player = voiceBroadcastPlayer,
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
|||
|
||||
import im.vector.app.core.utils.TextUtils
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||
import im.vector.app.features.voicebroadcast.duration
|
||||
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
|
||||
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
|
@ -148,4 +149,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
|
|||
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
||||
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
|
||||
}
|
||||
|
||||
fun getDuration(): Int {
|
||||
return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.reduceOrNull { acc, duration -> acc + duration } ?: 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
data class Attributes(
|
||||
val voiceBroadcastId: String,
|
||||
val voiceBroadcastState: VoiceBroadcastState?,
|
||||
val duration: Int,
|
||||
val recorderName: String,
|
||||
val recorder: VoiceBroadcastRecorder?,
|
||||
val player: VoiceBroadcastPlayer,
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.SeekBar
|
||||
|
@ -25,7 +26,7 @@ import androidx.core.view.isVisible
|
|||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||
|
||||
|
@ -44,6 +45,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
renderPlayingState(holder, state)
|
||||
}
|
||||
player.addListener(voiceBroadcastId, playerListener)
|
||||
bindSeekBar(holder)
|
||||
}
|
||||
|
||||
override fun renderMetadata(holder: Holder) {
|
||||
|
@ -66,24 +68,39 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
VoiceBroadcastPlayer.State.PLAYING -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
||||
playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
|
||||
playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) }
|
||||
}
|
||||
VoiceBroadcastPlayer.State.IDLE,
|
||||
VoiceBroadcastPlayer.State.PAUSED -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
||||
playPauseButton.onClick {
|
||||
callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
|
||||
}
|
||||
playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) }
|
||||
}
|
||||
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindSeekBar(holder: Holder) {
|
||||
holder.durationView.text = formatPlaybackTime(voiceBroadcastAttributes.duration)
|
||||
holder.seekBar.max = voiceBroadcastAttributes.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 onStopTrackingTouch(seekBar: SeekBar) {
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
super.unbind(holder)
|
||||
player.removeListener(voiceBroadcastId, playerListener)
|
||||
holder.seekBar.setOnSeekBarChangeListener(null)
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
|
|
@ -32,3 +32,5 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? {
|
|||
}
|
||||
|
||||
val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence
|
||||
|
||||
val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0
|
||||
|
|
|
@ -41,9 +41,15 @@ class VoiceBroadcastHelper @Inject constructor(
|
|||
|
||||
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
|
||||
|
||||
fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.playOrResume(roomId, eventId)
|
||||
fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId)
|
||||
|
||||
fun pausePlayback() = voiceBroadcastPlayer.pause()
|
||||
|
||||
fun stopPlayback() = voiceBroadcastPlayer.stop()
|
||||
|
||||
fun seekTo(voiceBroadcastId: String, positionMillis: Int) {
|
||||
if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) {
|
||||
voiceBroadcastPlayer.seekTo(positionMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,11 @@ interface VoiceBroadcastPlayer {
|
|||
*/
|
||||
fun stop()
|
||||
|
||||
/**
|
||||
* Seek to the given playback position, is milliseconds.
|
||||
*/
|
||||
fun seekTo(positionMillis: Int)
|
||||
|
||||
/**
|
||||
* Add a [Listener] to the given voice broadcast id.
|
||||
*/
|
||||
|
|
|
@ -22,7 +22,7 @@ import androidx.annotation.MainThread
|
|||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.voice.VoiceFailure
|
||||
import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk
|
||||
import im.vector.app.features.voicebroadcast.duration
|
||||
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.usecase.GetLiveVoiceBroadcastChunksUseCase
|
||||
|
@ -37,6 +37,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.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
|
||||
|
@ -69,7 +70,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
private var currentSequence: Int? = null
|
||||
|
||||
private var fetchPlaylistJob: Job? = null
|
||||
private var playlist = emptyList<MessageAudioEvent>()
|
||||
private var playlist = emptyList<PlaylistItem>()
|
||||
|
||||
private var isLive: Boolean = false
|
||||
|
||||
override var currentVoiceBroadcastId: String? = null
|
||||
|
@ -170,8 +172,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun updatePlaylist(playlist: List<MessageAudioEvent>) {
|
||||
this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs }
|
||||
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)
|
||||
this.playlist = sorted.mapIndexed { index, messageAudioEvent ->
|
||||
PlaylistItem(
|
||||
audioEvent = messageAudioEvent,
|
||||
startTime = chunkPositions.getOrNull(index) ?: 0
|
||||
)
|
||||
}
|
||||
onPlaylistUpdated()
|
||||
}
|
||||
|
||||
|
@ -195,16 +207,23 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun startPlayback() {
|
||||
val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull()
|
||||
val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
||||
val sequence = event.getVoiceBroadcastChunk()?.sequence
|
||||
private fun startPlayback(sequence: Int? = null, position: Int = 0) {
|
||||
val playlistItem = when {
|
||||
sequence != null -> playlist.find { it.audioEvent.sequence == sequence }
|
||||
isLive -> playlist.lastOrNull()
|
||||
else -> playlist.firstOrNull()
|
||||
}
|
||||
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
||||
val computedSequence = playlistItem.audioEvent.sequence
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
currentMediaPlayer = prepareMediaPlayer(content)
|
||||
currentMediaPlayer?.start()
|
||||
if (position > 0) {
|
||||
currentMediaPlayer?.seekTo(position)
|
||||
}
|
||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||
currentSequence = sequence
|
||||
currentSequence = computedSequence
|
||||
withContext(Dispatchers.Main) { playingState = State.PLAYING }
|
||||
nextMediaPlayer = prepareNextMediaPlayer()
|
||||
} catch (failure: Throwable) {
|
||||
|
@ -220,11 +239,27 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
playingState = State.PLAYING
|
||||
}
|
||||
|
||||
override fun seekTo(positionMillis: Int) {
|
||||
val duration = getVoiceBroadcastDuration()
|
||||
val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return
|
||||
val chunk = playlistItem.audioEvent
|
||||
val chunkPosition = positionMillis - playlistItem.startTime
|
||||
|
||||
Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${chunk.sequence}, sequencePosition=$chunkPosition")
|
||||
|
||||
tryOrNull { currentMediaPlayer?.stop() }
|
||||
release(currentMediaPlayer)
|
||||
tryOrNull { nextMediaPlayer?.stop() }
|
||||
release(nextMediaPlayer)
|
||||
|
||||
startPlayback(chunk.sequence, chunkPosition)
|
||||
}
|
||||
|
||||
private fun getNextAudioContent(): MessageAudioContent? {
|
||||
val nextSequence = currentSequence?.plus(1)
|
||||
?: playlist.lastOrNull()?.sequence
|
||||
?: playlist.lastOrNull()?.audioEvent?.sequence
|
||||
?: 1
|
||||
return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content
|
||||
return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content
|
||||
}
|
||||
|
||||
private suspend fun prepareNextMediaPlayer(): MediaPlayer? {
|
||||
|
@ -302,4 +337,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0
|
||||
|
||||
private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue