Merge pull request #7496 from vector-im/feature/fre/voice_broadcast_seek_to_live_update
Voice Broadcast - Update seek bar position while playing
This commit is contained in:
commit
271fd05a1c
|
@ -0,0 +1 @@
|
|||
[Voice Broadcast] Add seekbar in listening tile
|
|
@ -103,14 +103,12 @@ class VideoViewHolder constructor(itemView: View) :
|
|||
views.videoView.setOnPreparedListener {
|
||||
stopTimer()
|
||||
countUpTimer = CountUpTimer(100).also {
|
||||
it.tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val duration = views.videoView.duration
|
||||
val progress = views.videoView.currentPosition
|
||||
val isPlaying = views.videoView.isPlaying
|
||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
it.tickListener = CountUpTimer.TickListener {
|
||||
val duration = views.videoView.duration
|
||||
val progress = views.videoView.currentPosition
|
||||
val isPlaying = views.videoView.isPlaying
|
||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
it.resume()
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) {
|
|||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
interface TickListener {
|
||||
fun interface TickListener {
|
||||
fun onTick(milliseconds: Long)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,12 +167,10 @@ class WebRtcCall(
|
|||
private var screenSender: RtpSender? = null
|
||||
|
||||
private val timer = CountUpTimer(1000L).apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onTick(formattedDuration) }
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { milliseconds ->
|
||||
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onTick(formattedDuration) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
|||
import android.view.View
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.call.conference.ConferenceEvent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
|
@ -129,10 +130,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
}
|
||||
|
||||
sealed class Listening : VoiceBroadcastAction() {
|
||||
data class PlayOrResume(val voiceBroadcastId: String) : Listening()
|
||||
data class PlayOrResume(val voiceBroadcast: VoiceBroadcast) : Listening()
|
||||
object Pause : Listening()
|
||||
object Stop : Listening()
|
||||
data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening()
|
||||
data class SeekTo(val voiceBroadcast: VoiceBroadcast, val positionMillis: Int, val duration: Int) : Listening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -634,10 +634,10 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
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)
|
||||
is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast)
|
||||
VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
|
||||
VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
|
||||
is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis)
|
||||
is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcast, action.positionMillis, action.duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,11 +199,7 @@ class AudioMessageHelper @Inject constructor(
|
|||
private fun startRecordingAmplitudes() {
|
||||
amplitudeTicker?.stop()
|
||||
amplitudeTicker = CountUpTimer(50).apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
onAmplitudeTick()
|
||||
}
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { onAmplitudeTick() }
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
@ -234,11 +230,7 @@ class AudioMessageHelper @Inject constructor(
|
|||
private fun startPlaybackTicker(id: String) {
|
||||
playbackTicker?.stop()
|
||||
playbackTicker = CountUpTimer().apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
onPlaybackTick(id)
|
||||
}
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { onPlaybackTick(id) }
|
||||
resume()
|
||||
}
|
||||
onPlaybackTick(id)
|
||||
|
|
|
@ -189,11 +189,9 @@ class VoiceMessageRecorderView @JvmOverloads constructor(
|
|||
val startMs = ((clock.epochMillis() - startAt)).coerceAtLeast(0)
|
||||
recordingTicker?.stop()
|
||||
recordingTicker = CountUpTimer().apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
|
||||
onRecordingTick(isLocked, milliseconds + startMs)
|
||||
}
|
||||
tickListener = CountUpTimer.TickListener { milliseconds ->
|
||||
val isLocked = startFromLocked || lastKnownState is RecordingUiState.Locked
|
||||
onRecordingTick(isLocked, milliseconds + startMs)
|
||||
}
|
||||
resume()
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory
|
|||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.DrawableProvider
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
|
@ -28,6 +29,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
|
@ -44,6 +46,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
private val drawableProvider: DrawableProvider,
|
||||
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
|
||||
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
|
@ -58,19 +61,20 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
||||
val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
|
||||
val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
|
||||
val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId
|
||||
val voiceBroadcast = VoiceBroadcast(voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId, roomId = params.event.roomId)
|
||||
|
||||
val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED &&
|
||||
voiceBroadcastEvent.root.stateKey == session.myUserId &&
|
||||
messageContent.deviceId == session.sessionParams.deviceId
|
||||
|
||||
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
|
||||
voiceBroadcastId = voiceBroadcastId,
|
||||
voiceBroadcast = voiceBroadcast,
|
||||
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
||||
duration = voiceBroadcastEventsGroup.getDuration(),
|
||||
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
||||
recorder = voiceBroadcastRecorder,
|
||||
player = voiceBroadcastPlayer,
|
||||
playbackTracker = playbackTracker,
|
||||
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
||||
colorProvider = colorProvider,
|
||||
drawableProvider = drawableProvider,
|
||||
|
@ -89,7 +93,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||
): MessageVoiceBroadcastRecordingItem {
|
||||
return MessageVoiceBroadcastRecordingItem_()
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
|
||||
.attributes(attributes)
|
||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||
.highlighted(highlight)
|
||||
|
@ -102,7 +106,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||
): MessageVoiceBroadcastListeningItem {
|
||||
return MessageVoiceBroadcastListeningItem_()
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcast.voiceBroadcastId}")
|
||||
.attributes(attributes)
|
||||
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||
.highlighted(highlight)
|
||||
|
|
|
@ -127,7 +127,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getPercentage(id: String): Float {
|
||||
fun getPercentage(id: String): Float {
|
||||
return when (val state = states[id]) {
|
||||
is Listener.State.Playing -> state.percentage
|
||||
is Listener.State.Paused -> state.percentage
|
||||
|
@ -148,7 +148,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
const val RECORDING_ID = "RECORDING_ID"
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun interface Listener {
|
||||
|
||||
fun onUpdate(state: State)
|
||||
|
||||
|
|
|
@ -25,7 +25,9 @@ import im.vector.app.R
|
|||
import im.vector.app.core.extensions.tintBackground
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.DrawableProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
@ -35,11 +37,13 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
@EpoxyAttribute
|
||||
lateinit var voiceBroadcastAttributes: Attributes
|
||||
|
||||
protected val voiceBroadcastId get() = voiceBroadcastAttributes.voiceBroadcastId
|
||||
protected val voiceBroadcast get() = voiceBroadcastAttributes.voiceBroadcast
|
||||
protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState
|
||||
protected val recorderName get() = voiceBroadcastAttributes.recorderName
|
||||
protected val recorder get() = voiceBroadcastAttributes.recorder
|
||||
protected val player get() = voiceBroadcastAttributes.player
|
||||
protected val playbackTracker get() = voiceBroadcastAttributes.playbackTracker
|
||||
protected val duration get() = voiceBroadcastAttributes.duration
|
||||
protected val roomItem get() = voiceBroadcastAttributes.roomItem
|
||||
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
||||
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
||||
|
@ -92,12 +96,13 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
|||
}
|
||||
|
||||
data class Attributes(
|
||||
val voiceBroadcastId: String,
|
||||
val voiceBroadcast: VoiceBroadcast,
|
||||
val voiceBroadcastState: VoiceBroadcastState?,
|
||||
val duration: Int,
|
||||
val recorderName: String,
|
||||
val recorder: VoiceBroadcastRecorder?,
|
||||
val player: VoiceBroadcastPlayer,
|
||||
val playbackTracker: AudioMessagePlaybackTracker,
|
||||
val roomItem: MatrixItem?,
|
||||
val colorProvider: ColorProvider,
|
||||
val drawableProvider: DrawableProvider,
|
||||
|
|
|
@ -140,16 +140,14 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
|
|||
}
|
||||
|
||||
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderIdleState(holder: Holder) {
|
||||
|
|
|
@ -27,6 +27,7 @@ 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.VoiceBroadcastAction
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||
|
||||
|
@ -34,6 +35,7 @@ import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
|||
abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastListeningItem.Holder>() {
|
||||
|
||||
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
|
||||
private var isUserSeeking = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
@ -41,11 +43,35 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
|
||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||
playerListener = VoiceBroadcastPlayer.Listener { state ->
|
||||
renderPlayingState(holder, state)
|
||||
}
|
||||
player.addListener(voiceBroadcastId, playerListener)
|
||||
playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) }
|
||||
player.addListener(voiceBroadcast, playerListener)
|
||||
bindSeekBar(holder)
|
||||
bindButtons(holder)
|
||||
}
|
||||
|
||||
private fun bindButtons(holder: Holder) {
|
||||
with(holder) {
|
||||
playPauseButton.onClick {
|
||||
if (player.currentVoiceBroadcast == voiceBroadcast) {
|
||||
when (player.playingState) {
|
||||
VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
||||
VoiceBroadcastPlayer.State.PAUSED,
|
||||
VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
||||
}
|
||||
} else {
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||
}
|
||||
}
|
||||
fastBackwardButton.onClick {
|
||||
val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration)
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
||||
}
|
||||
fastForwardButton.onClick {
|
||||
val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration)
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun renderMetadata(holder: Holder) {
|
||||
|
@ -61,50 +87,67 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
|||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
||||
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
||||
|
||||
fastBackwardButton.isInvisible = true
|
||||
fastForwardButton.isInvisible = true
|
||||
|
||||
when (state) {
|
||||
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(VoiceBroadcastAction.Listening.Pause) }
|
||||
seekBar.isEnabled = true
|
||||
}
|
||||
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(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) }
|
||||
seekBar.isEnabled = false
|
||||
}
|
||||
VoiceBroadcastPlayer.State.BUFFERING -> {
|
||||
seekBar.isEnabled = true
|
||||
}
|
||||
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
|
||||
with(holder) {
|
||||
durationView.text = formatPlaybackTime(duration)
|
||||
seekBar.max = duration
|
||||
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) = Unit
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
isUserSeeking = true
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress))
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, seekBar.progress, duration))
|
||||
isUserSeeking = false
|
||||
}
|
||||
})
|
||||
}
|
||||
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
||||
renderBackwardForwardButtons(holder, playbackState)
|
||||
if (!isUserSeeking) {
|
||||
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
|
||||
val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused
|
||||
val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
|
||||
val canBackward = isPlayingOrPaused && playbackTime > 0
|
||||
val canForward = isPlayingOrPaused && playbackTime < duration
|
||||
holder.fastBackwardButton.isInvisible = !canBackward
|
||||
holder.fastForwardButton.isInvisible = !canForward
|
||||
}
|
||||
|
||||
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)
|
||||
player.removeListener(voiceBroadcast, playerListener)
|
||||
playbackTracker.untrack(voiceBroadcast.voiceBroadcastId)
|
||||
with(holder) {
|
||||
seekBar.onClick(null)
|
||||
playPauseButton.onClick(null)
|
||||
fastForwardButton.onClick(null)
|
||||
fastBackwardButton.onClick(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
|
|
@ -122,16 +122,14 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
true
|
||||
}
|
||||
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
||||
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state ->
|
||||
when (state) {
|
||||
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed)
|
||||
is AudioMessagePlaybackTracker.Listener.State.Recording -> Unit
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = (motionEvent.x / view.width).coerceIn(0f, 1f)
|
||||
|
|
|
@ -79,10 +79,8 @@ abstract class LiveLocationUserItem : VectorEpoxyModel<LiveLocationUserItem.Hold
|
|||
}
|
||||
}
|
||||
|
||||
holder.timer.tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
|
||||
}
|
||||
holder.timer.tickListener = CountUpTimer.TickListener {
|
||||
holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
|
||||
}
|
||||
holder.timer.resume()
|
||||
|
||||
|
|
|
@ -16,7 +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
|
||||
|
@ -34,3 +38,9 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? {
|
|||
val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence
|
||||
|
||||
val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0
|
||||
|
||||
val VoiceBroadcastEvent.isLive
|
||||
get() = content?.isLive.orFalse()
|
||||
|
||||
val MessageVoiceBroadcastInfoContent.isLive
|
||||
get() = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.voicebroadcast
|
||||
|
||||
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
|
||||
|
@ -41,15 +42,13 @@ class VoiceBroadcastHelper @Inject constructor(
|
|||
|
||||
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
|
||||
|
||||
fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId)
|
||||
fun playOrResumePlayback(voiceBroadcast: VoiceBroadcast) = voiceBroadcastPlayer.playOrResume(voiceBroadcast)
|
||||
|
||||
fun pausePlayback() = voiceBroadcastPlayer.pause()
|
||||
|
||||
fun stopPlayback() = voiceBroadcastPlayer.stop()
|
||||
|
||||
fun seekTo(voiceBroadcastId: String, positionMillis: Int) {
|
||||
if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) {
|
||||
voiceBroadcastPlayer.seekTo(positionMillis)
|
||||
}
|
||||
fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
|
||||
voiceBroadcastPlayer.seekTo(voiceBroadcast, positionMillis, duration)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
|
||||
package im.vector.app.features.voicebroadcast.listening
|
||||
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
|
||||
interface VoiceBroadcastPlayer {
|
||||
|
||||
/**
|
||||
* The current playing voice broadcast identifier, if any.
|
||||
* The current playing voice broadcast, if any.
|
||||
*/
|
||||
val currentVoiceBroadcastId: String?
|
||||
val currentVoiceBroadcast: VoiceBroadcast?
|
||||
|
||||
/**
|
||||
* The current playing [State], [State.IDLE] by default.
|
||||
|
@ -31,7 +33,7 @@ interface VoiceBroadcastPlayer {
|
|||
/**
|
||||
* Start playback of the given voice broadcast.
|
||||
*/
|
||||
fun playOrResume(roomId: String, voiceBroadcastId: String)
|
||||
fun playOrResume(voiceBroadcast: VoiceBroadcast)
|
||||
|
||||
/**
|
||||
* Pause playback of the current voice broadcast, if any.
|
||||
|
@ -44,19 +46,19 @@ interface VoiceBroadcastPlayer {
|
|||
fun stop()
|
||||
|
||||
/**
|
||||
* Seek to the given playback position, is milliseconds.
|
||||
* Seek the given voice broadcast playback to the given position, is milliseconds.
|
||||
*/
|
||||
fun seekTo(positionMillis: Int)
|
||||
fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int)
|
||||
|
||||
/**
|
||||
* Add a [Listener] to the given voice broadcast id.
|
||||
* Add a [Listener] to the given voice broadcast.
|
||||
*/
|
||||
fun addListener(voiceBroadcastId: String, listener: Listener)
|
||||
fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener)
|
||||
|
||||
/**
|
||||
* Remove a [Listener] from the given voice broadcast id.
|
||||
* Remove a [Listener] from the given voice broadcast.
|
||||
*/
|
||||
fun removeListener(voiceBroadcastId: String, listener: Listener)
|
||||
fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener)
|
||||
|
||||
/**
|
||||
* Player states.
|
||||
|
|
|
@ -18,28 +18,28 @@ package im.vector.app.features.voicebroadcast.listening
|
|||
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.media.MediaPlayer.OnPreparedListener
|
||||
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.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
|
||||
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.sequence
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
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.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
|
||||
|
@ -49,179 +49,161 @@ import javax.inject.Singleton
|
|||
class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||
private val sessionHolder: ActiveSessionHolder,
|
||||
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
||||
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
|
||||
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
|
||||
) : VoiceBroadcastPlayer {
|
||||
|
||||
private val session
|
||||
get() = sessionHolder.getActiveSession()
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private var voiceBroadcastStateJob: Job? = null
|
||||
private val session get() = sessionHolder.getActiveSession()
|
||||
private val sessionScope get() = session.coroutineScope
|
||||
|
||||
private val mediaPlayerListener = MediaPlayerListener()
|
||||
private val playbackTicker = PlaybackTicker()
|
||||
private val playlist = VoiceBroadcastPlaylist()
|
||||
|
||||
private var fetchPlaylistTask: Job? = null
|
||||
private var voiceBroadcastStateObserver: Job? = null
|
||||
|
||||
private var currentMediaPlayer: MediaPlayer? = null
|
||||
private var nextMediaPlayer: MediaPlayer? = null
|
||||
private var currentSequence: Int? = null
|
||||
private var isPreparingNextPlayer: Boolean = false
|
||||
|
||||
private var fetchPlaylistJob: Job? = null
|
||||
private var playlist = emptyList<PlaylistItem>()
|
||||
private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
|
||||
|
||||
private var isLive: Boolean = false
|
||||
|
||||
override var currentVoiceBroadcastId: String? = null
|
||||
override var currentVoiceBroadcast: VoiceBroadcast? = null
|
||||
|
||||
override var playingState = State.IDLE
|
||||
@MainThread
|
||||
set(value) {
|
||||
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
||||
field = value
|
||||
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||
currentVoiceBroadcastId?.let { voiceBroadcastId ->
|
||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) }
|
||||
if (field != value) {
|
||||
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
||||
field = value
|
||||
onPlayingStateChanged(value)
|
||||
}
|
||||
}
|
||||
private var currentRoomId: String? = null
|
||||
|
||||
/**
|
||||
* Map voiceBroadcastId to listeners.
|
||||
*/
|
||||
/** Map voiceBroadcastId to listeners.*/
|
||||
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
||||
|
||||
override fun playOrResume(roomId: String, voiceBroadcastId: String) {
|
||||
val hasChanged = currentVoiceBroadcastId != voiceBroadcastId
|
||||
override fun playOrResume(voiceBroadcast: VoiceBroadcast) {
|
||||
val hasChanged = currentVoiceBroadcast != voiceBroadcast
|
||||
when {
|
||||
hasChanged -> startPlayback(roomId, voiceBroadcastId)
|
||||
hasChanged -> startPlayback(voiceBroadcast)
|
||||
playingState == State.PAUSED -> resumePlayback()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
currentMediaPlayer?.pause()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
|
||||
playingState = State.PAUSED
|
||||
pausePlayback()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
// Stop playback
|
||||
currentMediaPlayer?.stop()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
|
||||
isLive = false
|
||||
|
||||
// Release current player
|
||||
release(currentMediaPlayer)
|
||||
currentMediaPlayer = null
|
||||
|
||||
// Release next player
|
||||
release(nextMediaPlayer)
|
||||
nextMediaPlayer = null
|
||||
|
||||
// Do not observe anymore voice broadcast state changes
|
||||
voiceBroadcastStateJob?.cancel()
|
||||
voiceBroadcastStateJob = null
|
||||
|
||||
// Do not fetch the playlist anymore
|
||||
fetchPlaylistJob?.cancel()
|
||||
fetchPlaylistJob = null
|
||||
|
||||
// Update state
|
||||
playingState = State.IDLE
|
||||
|
||||
// Stop and release media players
|
||||
stopPlayer()
|
||||
|
||||
// Do not observe anymore voice broadcast changes
|
||||
fetchPlaylistTask?.cancel()
|
||||
fetchPlaylistTask = null
|
||||
voiceBroadcastStateObserver?.cancel()
|
||||
voiceBroadcastStateObserver = null
|
||||
|
||||
// Clear playlist
|
||||
playlist = emptyList()
|
||||
currentSequence = null
|
||||
playlist.reset()
|
||||
|
||||
currentRoomId = null
|
||||
currentVoiceBroadcastId = null
|
||||
currentVoiceBroadcastEvent = null
|
||||
currentVoiceBroadcast = null
|
||||
}
|
||||
|
||||
override fun addListener(voiceBroadcastId: String, listener: Listener) {
|
||||
listeners[voiceBroadcastId]?.add(listener) ?: run {
|
||||
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||
override fun addListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
||||
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
||||
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||
}
|
||||
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE)
|
||||
listener.onStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
|
||||
}
|
||||
|
||||
override fun removeListener(voiceBroadcastId: String, listener: Listener) {
|
||||
listeners[voiceBroadcastId]?.remove(listener)
|
||||
override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
||||
listeners[voiceBroadcast.voiceBroadcastId]?.remove(listener)
|
||||
}
|
||||
|
||||
private fun startPlayback(roomId: String, eventId: String) {
|
||||
private fun startPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||
// Stop listening previous voice broadcast if any
|
||||
if (playingState != State.IDLE) stop()
|
||||
|
||||
currentRoomId = roomId
|
||||
currentVoiceBroadcastId = eventId
|
||||
currentVoiceBroadcast = voiceBroadcast
|
||||
|
||||
playingState = State.BUFFERING
|
||||
|
||||
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
|
||||
isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||
fetchPlaylistAndStartPlayback(roomId, eventId)
|
||||
observeVoiceBroadcastLiveState(voiceBroadcast)
|
||||
fetchPlaylistAndStartPlayback(voiceBroadcast)
|
||||
}
|
||||
|
||||
private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) {
|
||||
fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId)
|
||||
.onEach(this::updatePlaylist)
|
||||
.launchIn(coroutineScope)
|
||||
private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
|
||||
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
|
||||
.onEach { currentVoiceBroadcastEvent = it.getOrNull() }
|
||||
.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 fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
|
||||
fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
|
||||
.onEach {
|
||||
playlist.setItems(it)
|
||||
onPlaylistUpdated()
|
||||
}
|
||||
.launchIn(sessionScope)
|
||||
}
|
||||
|
||||
private fun onPlaylistUpdated() {
|
||||
when (playingState) {
|
||||
State.PLAYING -> {
|
||||
if (nextMediaPlayer == null) {
|
||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
State.PAUSED -> {
|
||||
if (nextMediaPlayer == null) {
|
||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||
if (nextMediaPlayer == null && !isPreparingNextPlayer) {
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
State.BUFFERING -> {
|
||||
val newMediaContent = getNextAudioContent()
|
||||
if (newMediaContent != null) startPlayback()
|
||||
val nextItem = playlist.getNextItem()
|
||||
if (nextItem != null) {
|
||||
val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
|
||||
startPlayback(savedPosition?.takeIf { it > 0 })
|
||||
}
|
||||
}
|
||||
State.IDLE -> {
|
||||
val savedPosition = currentVoiceBroadcast?.let { playbackTracker.getPlaybackTime(it.voiceBroadcastId) }
|
||||
startPlayback(savedPosition?.takeIf { it > 0 })
|
||||
}
|
||||
State.IDLE -> startPlayback()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPlayback(sequence: Int? = null, position: Int = 0) {
|
||||
private fun startPlayback(position: Int? = null) {
|
||||
stopPlayer()
|
||||
|
||||
val playlistItem = when {
|
||||
sequence != null -> playlist.find { it.audioEvent.sequence == sequence }
|
||||
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 computedSequence = playlistItem.audioEvent.sequence
|
||||
coroutineScope.launch {
|
||||
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 {
|
||||
currentMediaPlayer = prepareMediaPlayer(content)
|
||||
currentMediaPlayer?.start()
|
||||
if (position > 0) {
|
||||
currentMediaPlayer?.seekTo(position)
|
||||
prepareMediaPlayer(content) { mp ->
|
||||
currentMediaPlayer = mp
|
||||
playlist.currentSequence = sequence
|
||||
mp.start()
|
||||
if (sequencePosition > 0) {
|
||||
mp.seekTo(sequencePosition)
|
||||
}
|
||||
playingState = State.PLAYING
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||
currentSequence = computedSequence
|
||||
withContext(Dispatchers.Main) { playingState = State.PLAYING }
|
||||
nextMediaPlayer = prepareNextMediaPlayer()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Unable to start playback")
|
||||
throw VoiceFailure.UnableToPlay(failure)
|
||||
|
@ -229,41 +211,59 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun pausePlayback(positionMillis: Int? = null) {
|
||||
if (positionMillis == null) {
|
||||
currentMediaPlayer?.pause()
|
||||
} else {
|
||||
stopPlayer()
|
||||
val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId
|
||||
val duration = playlist.duration.takeIf { it > 0 }
|
||||
if (voiceBroadcastId != null && duration != null) {
|
||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||
}
|
||||
}
|
||||
playingState = State.PAUSED
|
||||
}
|
||||
|
||||
private fun resumePlayback() {
|
||||
currentMediaPlayer?.start()
|
||||
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
|
||||
playingState = State.PLAYING
|
||||
if (currentMediaPlayer != null) {
|
||||
currentMediaPlayer?.start()
|
||||
playingState = State.PLAYING
|
||||
} else {
|
||||
val position = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
|
||||
startPlayback(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTo(positionMillis: Int) {
|
||||
val duration = getVoiceBroadcastDuration()
|
||||
val playlistItem = playlist.lastOrNull { it.startTime <= positionMillis } ?: return
|
||||
val audioEvent = playlistItem.audioEvent
|
||||
val eventPosition = positionMillis - playlistItem.startTime
|
||||
|
||||
Timber.d("## Voice Broadcast | seekTo - duration=$duration, position=$positionMillis, sequence=${audioEvent.sequence}, sequencePosition=$eventPosition")
|
||||
|
||||
tryOrNull { currentMediaPlayer?.stop() }
|
||||
release(currentMediaPlayer)
|
||||
tryOrNull { nextMediaPlayer?.stop() }
|
||||
release(nextMediaPlayer)
|
||||
|
||||
startPlayback(audioEvent.sequence, eventPosition)
|
||||
override fun seekTo(voiceBroadcast: VoiceBroadcast, positionMillis: Int, duration: Int) {
|
||||
when {
|
||||
voiceBroadcast != currentVoiceBroadcast -> {
|
||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||
}
|
||||
playingState == State.PLAYING || playingState == State.BUFFERING -> {
|
||||
startPlayback(positionMillis)
|
||||
}
|
||||
playingState == State.IDLE || playingState == State.PAUSED -> {
|
||||
pausePlayback(positionMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextAudioContent(): MessageAudioContent? {
|
||||
val nextSequence = currentSequence?.plus(1)
|
||||
?: playlist.lastOrNull()?.audioEvent?.sequence
|
||||
?: 1
|
||||
return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content
|
||||
private fun prepareNextMediaPlayer() {
|
||||
val nextItem = playlist.getNextItem()
|
||||
if (nextItem != null) {
|
||||
isPreparingNextPlayer = true
|
||||
sessionScope.launch {
|
||||
prepareMediaPlayer(nextItem.audioEvent.content) { mp ->
|
||||
nextMediaPlayer = mp
|
||||
currentMediaPlayer?.setNextMediaPlayer(mp)
|
||||
isPreparingNextPlayer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareNextMediaPlayer(): MediaPlayer? {
|
||||
val nextContent = getNextAudioContent() ?: return null
|
||||
return prepareMediaPlayer(nextContent)
|
||||
}
|
||||
|
||||
private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer {
|
||||
private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent, onPreparedListener: OnPreparedListener): MediaPlayer {
|
||||
// Download can fail
|
||||
val audioFile = try {
|
||||
session.fileService().downloadFile(messageAudioContent)
|
||||
|
@ -284,58 +284,76 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
setDataSource(fis.fd)
|
||||
setOnInfoListener(mediaPlayerListener)
|
||||
setOnErrorListener(mediaPlayerListener)
|
||||
setOnPreparedListener(onPreparedListener)
|
||||
setOnCompletionListener(mediaPlayerListener)
|
||||
prepare()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun release(mp: MediaPlayer?) {
|
||||
mp?.apply {
|
||||
release()
|
||||
setOnInfoListener(null)
|
||||
setOnCompletionListener(null)
|
||||
setOnErrorListener(null)
|
||||
private fun stopPlayer() {
|
||||
tryOrNull { currentMediaPlayer?.stop() }
|
||||
currentMediaPlayer?.release()
|
||||
currentMediaPlayer = null
|
||||
|
||||
nextMediaPlayer?.release()
|
||||
nextMediaPlayer = null
|
||||
isPreparingNextPlayer = false
|
||||
}
|
||||
|
||||
private fun onPlayingStateChanged(playingState: State) {
|
||||
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
||||
when (playingState) {
|
||||
State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
||||
State.PAUSED,
|
||||
State.BUFFERING,
|
||||
State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
||||
}
|
||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentPlaybackPosition(): Int? {
|
||||
val playlistPosition = playlist.currentItem?.startTime
|
||||
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
||||
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
|
||||
return computedPosition ?: savedPosition
|
||||
}
|
||||
|
||||
private fun getCurrentPlaybackPercentage(): Float? {
|
||||
val playlistPosition = playlist.currentItem?.startTime
|
||||
val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
|
||||
val duration = playlist.duration.takeIf { it > 0 }
|
||||
val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null
|
||||
val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) }
|
||||
return computedPercentage ?: savedPercentage
|
||||
}
|
||||
|
||||
private inner class MediaPlayerListener :
|
||||
MediaPlayer.OnInfoListener,
|
||||
MediaPlayer.OnPreparedListener,
|
||||
MediaPlayer.OnCompletionListener,
|
||||
MediaPlayer.OnErrorListener {
|
||||
|
||||
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||
when (what) {
|
||||
MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> {
|
||||
release(currentMediaPlayer)
|
||||
playlist.currentSequence = playlist.currentSequence?.inc()
|
||||
currentMediaPlayer = mp
|
||||
currentSequence = currentSequence?.plus(1)
|
||||
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
|
||||
nextMediaPlayer = null
|
||||
playingState = State.PLAYING
|
||||
prepareNextMediaPlayer()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPrepared(mp: MediaPlayer) {
|
||||
when (mp) {
|
||||
currentMediaPlayer -> {
|
||||
nextMediaPlayer?.let { mp.setNextMediaPlayer(it) }
|
||||
}
|
||||
nextMediaPlayer -> {
|
||||
tryOrNull { currentMediaPlayer?.setNextMediaPlayer(mp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompletion(mp: MediaPlayer) {
|
||||
if (nextMediaPlayer != null) return
|
||||
val roomId = currentRoomId ?: return
|
||||
val voiceBroadcastId = currentVoiceBroadcastId ?: return
|
||||
val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return
|
||||
isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED
|
||||
|
||||
if (!isLive && voiceBroadcastEventContent.lastChunkSequence == 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 {
|
||||
|
@ -349,7 +367,48 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0
|
||||
private inner class PlaybackTicker(
|
||||
private var playbackTicker: CountUpTimer? = null,
|
||||
) {
|
||||
|
||||
private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)
|
||||
fun startPlaybackTicker(id: String) {
|
||||
playbackTicker?.stop()
|
||||
playbackTicker = CountUpTimer(50L).apply {
|
||||
tickListener = CountUpTimer.TickListener { onPlaybackTick(id) }
|
||||
resume()
|
||||
}
|
||||
onPlaybackTick(id)
|
||||
}
|
||||
|
||||
fun stopPlaybackTicker(id: String) {
|
||||
playbackTicker?.stop()
|
||||
playbackTicker = null
|
||||
onPlaybackTick(id)
|
||||
}
|
||||
|
||||
private fun onPlaybackTick(id: String) {
|
||||
val playbackTime = getCurrentPlaybackPosition()
|
||||
val percentage = getCurrentPlaybackPercentage()
|
||||
when (playingState) {
|
||||
State.PLAYING -> {
|
||||
if (playbackTime != null && percentage != null) {
|
||||
playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage)
|
||||
}
|
||||
}
|
||||
State.PAUSED,
|
||||
State.BUFFERING -> {
|
||||
if (playbackTime != null && percentage != null) {
|
||||
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||
}
|
||||
}
|
||||
State.IDLE -> {
|
||||
if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) {
|
||||
playbackTracker.stopPlayback(id)
|
||||
} else {
|
||||
playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: Int? = null
|
||||
val currentItem get() = currentSequence?.let { findBySequence(it) }
|
||||
|
||||
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 = null
|
||||
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) ?: 1)
|
||||
|
||||
fun firstOrNull() = findBySequence(1)
|
||||
}
|
||||
|
||||
data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)
|
|
@ -19,18 +19,21 @@ package im.vector.app.features.voicebroadcast.listening.usecase
|
|||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
|
||||
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.sequence
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.runningReduce
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||
|
@ -44,19 +47,19 @@ import javax.inject.Inject
|
|||
*/
|
||||
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
|
||||
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
|
||||
) {
|
||||
|
||||
fun execute(roomId: String, voiceBroadcastId: String): Flow<List<MessageAudioEvent>> {
|
||||
fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<MessageAudioEvent>> {
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
|
||||
val room = session.roomService().getRoom(roomId) ?: return emptyFlow()
|
||||
val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow()
|
||||
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
|
||||
|
||||
// Get initial chunks
|
||||
val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId)
|
||||
val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
|
||||
.mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } }
|
||||
|
||||
val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)
|
||||
val voiceBroadcastEvent = runBlocking { getVoiceBroadcastEventUseCase.execute(voiceBroadcast).firstOrNull()?.getOrNull() }
|
||||
val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
|
||||
|
||||
return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) {
|
||||
|
@ -82,7 +85,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
|||
lastSequence = stopEvent.content?.lastChunkSequence
|
||||
}
|
||||
|
||||
val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId)
|
||||
val newChunks = newEvents.mapToChunkEvents(voiceBroadcast.voiceBroadcastId, voiceBroadcastEvent.root.senderId)
|
||||
|
||||
// Notify about new chunks
|
||||
if (newChunks.isNotEmpty()) {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
data class VoiceBroadcast(
|
||||
val voiceBroadcastId: String,
|
||||
val roomId: String,
|
||||
)
|
|
@ -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.usecase
|
||||
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
import org.matrix.android.sdk.flow.unwrap
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetVoiceBroadcastEventUseCase @Inject constructor(
|
||||
private val session: Session,
|
||||
) {
|
||||
|
||||
fun execute(voiceBroadcast: VoiceBroadcast): Flow<Optional<VoiceBroadcastEvent>> {
|
||||
val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}")
|
||||
|
||||
Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast")
|
||||
|
||||
val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent()
|
||||
val latestEvent = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
|
||||
.mapNotNull { it.root.asVoiceBroadcastEvent() }
|
||||
.maxByOrNull { it.root.originServerTs ?: 0 }
|
||||
?: initialEvent
|
||||
|
||||
return when (latestEvent?.content?.voiceBroadcastState) {
|
||||
null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional())
|
||||
else -> {
|
||||
room.flow()
|
||||
.liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty()))
|
||||
.unwrap()
|
||||
.mapNotNull { it.asVoiceBroadcastEvent() }
|
||||
.filter { it.reference?.eventId == voiceBroadcast.voiceBroadcastId }
|
||||
.map { it.toOptional() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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.usecase
|
||||
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetVoiceBroadcastUseCase @Inject constructor(
|
||||
private val session: Session,
|
||||
) {
|
||||
|
||||
fun execute(roomId: String, eventId: String): VoiceBroadcastEvent? {
|
||||
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
|
||||
|
||||
Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $eventId")
|
||||
|
||||
val initialEvent = room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() // Fallback to initial event
|
||||
val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId).sortedBy { it.root.originServerTs }
|
||||
return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent
|
||||
}
|
||||
}
|
|
@ -100,10 +100,12 @@
|
|||
android:id="@+id/fastBackwardButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:background="@drawable/bg_rounded_button"
|
||||
android:contentDescription="@string/a11y_voice_broadcast_fast_backward"
|
||||
android:src="@drawable/ic_player_backward_30"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
android:visibility="invisible"
|
||||
app:tint="?vctr_content_secondary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/playPauseButton"
|
||||
|
@ -121,16 +123,20 @@
|
|||
android:layout_height="@dimen/voice_broadcast_player_button_size"
|
||||
android:contentDescription="@string/a11y_voice_broadcast_buffering"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="?vctr_content_secondary" />
|
||||
android:indeterminateTint="?vctr_content_secondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/fastForwardButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:background="@drawable/bg_rounded_button"
|
||||
android:contentDescription="@string/a11y_voice_broadcast_fast_forward"
|
||||
android:src="@drawable/ic_player_forward_30"
|
||||
app:tint="?vctr_content_secondary" />
|
||||
android:visibility="invisible"
|
||||
app:tint="?vctr_content_secondary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekBar"
|
||||
|
|
Loading…
Reference in New Issue