mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-21 13:48:46 +03:00
Merge pull request #7579 from vector-im/feature/fre/voice_broadcast_live_indicator
Voice Broadcast - Improve live indicator icon rendering
This commit is contained in:
commit
4ac9c8d0e0
10 changed files with 154 additions and 45 deletions
changelog.d
vector/src/main/java/im/vector/app/features
home/room/detail/timeline/item
AbsMessageVoiceBroadcastItem.ktMessageVoiceBroadcastListeningItem.ktMessageVoiceBroadcastRecordingItem.kt
voicebroadcast
1
changelog.d/7579.wip
Normal file
1
changelog.d/7579.wip
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Voice Broadcast] Improve the live indicator icon rendering in the timeline
|
|
@ -68,25 +68,26 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
|
||||||
renderMetadata(holder)
|
renderMetadata(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderLiveIndicator(holder: H) {
|
abstract fun renderLiveIndicator(holder: H)
|
||||||
|
|
||||||
|
protected fun renderPlayingLiveIndicator(holder: H) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
when (voiceBroadcastState) {
|
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
|
||||||
VoiceBroadcastState.STARTED,
|
liveIndicator.isVisible = true
|
||||||
VoiceBroadcastState.RESUMED -> {
|
|
||||||
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
|
|
||||||
liveIndicator.isVisible = true
|
|
||||||
}
|
|
||||||
VoiceBroadcastState.PAUSED -> {
|
|
||||||
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
|
|
||||||
liveIndicator.isVisible = true
|
|
||||||
}
|
|
||||||
VoiceBroadcastState.STOPPED, null -> {
|
|
||||||
liveIndicator.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun renderPausedLiveIndicator(holder: H) {
|
||||||
|
with(holder) {
|
||||||
|
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
|
||||||
|
liveIndicator.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun renderNoLiveIndicator(holder: H) {
|
||||||
|
holder.liveIndicator.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun renderMetadata(holder: H)
|
abstract fun renderMetadata(holder: H)
|
||||||
|
|
||||||
abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
|
abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
|
||||||
|
|
|
@ -29,6 +29,7 @@ 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.RoomDetailAction.VoiceBroadcastAction
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
|
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.listening.VoiceBroadcastPlayer
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
|
|
||||||
@EpoxyModelClass
|
@EpoxyModelClass
|
||||||
|
@ -43,7 +44,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||||
playerListener = VoiceBroadcastPlayer.Listener { renderPlayingState(holder, it) }
|
playerListener = object : VoiceBroadcastPlayer.Listener {
|
||||||
|
override fun onPlayingStateChanged(state: VoiceBroadcastPlayer.State) {
|
||||||
|
renderPlayingState(holder, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLiveModeChanged(isLive: Boolean) {
|
||||||
|
renderLiveIndicator(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
player.addListener(voiceBroadcast, playerListener)
|
player.addListener(voiceBroadcast, playerListener)
|
||||||
bindSeekBar(holder)
|
bindSeekBar(holder)
|
||||||
bindButtons(holder)
|
bindButtons(holder)
|
||||||
|
@ -51,7 +60,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
|
|
||||||
private fun bindButtons(holder: Holder) {
|
private fun bindButtons(holder: Holder) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
playPauseButton.onClick {
|
playPauseButton.setOnClickListener {
|
||||||
if (player.currentVoiceBroadcast == voiceBroadcast) {
|
if (player.currentVoiceBroadcast == voiceBroadcast) {
|
||||||
when (player.playingState) {
|
when (player.playingState) {
|
||||||
VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
|
||||||
|
@ -63,11 +72,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fastBackwardButton.onClick {
|
fastBackwardButton.setOnClickListener {
|
||||||
val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration)
|
val newPos = seekBar.progress.minus(30_000).coerceIn(0, duration)
|
||||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
||||||
}
|
}
|
||||||
fastForwardButton.onClick {
|
fastForwardButton.setOnClickListener {
|
||||||
val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration)
|
val newPos = seekBar.progress.plus(30_000).coerceIn(0, duration)
|
||||||
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcast, newPos, duration))
|
||||||
}
|
}
|
||||||
|
@ -82,6 +91,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun renderLiveIndicator(holder: Holder) {
|
||||||
|
when {
|
||||||
|
voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED -> renderNoLiveIndicator(holder)
|
||||||
|
voiceBroadcastState == VoiceBroadcastState.PAUSED || !player.isLiveListening -> renderPausedLiveIndicator(holder)
|
||||||
|
else -> renderPlayingLiveIndicator(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
||||||
|
@ -99,6 +116,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderLiveIndicator(holder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +140,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState ->
|
||||||
renderBackwardForwardButtons(holder, playbackState)
|
renderBackwardForwardButtons(holder, playbackState)
|
||||||
|
renderLiveIndicator(holder)
|
||||||
if (!isUserSeeking) {
|
if (!isUserSeeking) {
|
||||||
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
|
holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
|
||||||
}
|
}
|
||||||
|
@ -143,7 +163,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
|
||||||
player.removeListener(voiceBroadcast, playerListener)
|
player.removeListener(voiceBroadcast, playerListener)
|
||||||
playbackTracker.untrack(voiceBroadcast.voiceBroadcastId)
|
playbackTracker.untrack(voiceBroadcast.voiceBroadcastId)
|
||||||
with(holder) {
|
with(holder) {
|
||||||
seekBar.onClick(null)
|
seekBar.setOnSeekBarChangeListener(null)
|
||||||
playPauseButton.onClick(null)
|
playPauseButton.onClick(null)
|
||||||
fastForwardButton.onClick(null)
|
fastForwardButton.onClick(null)
|
||||||
fastBackwardButton.onClick(null)
|
fastBackwardButton.onClick(null)
|
||||||
|
|
|
@ -48,6 +48,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun renderLiveIndicator(holder: Holder) {
|
||||||
|
when (voiceBroadcastState) {
|
||||||
|
VoiceBroadcastState.STARTED,
|
||||||
|
VoiceBroadcastState.RESUMED -> renderPlayingLiveIndicator(holder)
|
||||||
|
VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder)
|
||||||
|
VoiceBroadcastState.STOPPED, null -> renderNoLiveIndicator(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun renderMetadata(holder: Holder) {
|
override fun renderMetadata(holder: Holder) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
listenersCountMetadata.isVisible = false
|
listenersCountMetadata.isVisible = false
|
||||||
|
@ -104,6 +113,10 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
recorderListener?.let { recorder?.removeListener(it) }
|
recorderListener?.let { recorder?.removeListener(it) }
|
||||||
recorderListener = null
|
recorderListener = null
|
||||||
|
with(holder) {
|
||||||
|
recordButton.onClick(null)
|
||||||
|
stopRecordButton.onClick(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
|
@ -39,6 +39,9 @@ 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.voiceBroadcastId
|
||||||
|
get() = reference?.eventId
|
||||||
|
|
||||||
val VoiceBroadcastEvent.isLive
|
val VoiceBroadcastEvent.isLive
|
||||||
get() = content?.isLive.orFalse()
|
get() = content?.isLive.orFalse()
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,11 @@ interface VoiceBroadcastPlayer {
|
||||||
*/
|
*/
|
||||||
val playingState: State
|
val playingState: State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether the player is listening a live voice broadcast in "live" position.
|
||||||
|
*/
|
||||||
|
val isLiveListening: Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start playback of the given voice broadcast.
|
* Start playback of the given voice broadcast.
|
||||||
*/
|
*/
|
||||||
|
@ -73,10 +78,15 @@ interface VoiceBroadcastPlayer {
|
||||||
/**
|
/**
|
||||||
* Listener related to [VoiceBroadcastPlayer].
|
* Listener related to [VoiceBroadcastPlayer].
|
||||||
*/
|
*/
|
||||||
fun interface Listener {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Notify about [VoiceBroadcastPlayer.playingState] changes.
|
* Notify about [VoiceBroadcastPlayer.playingState] changes.
|
||||||
*/
|
*/
|
||||||
fun onStateChanged(state: State)
|
fun onPlayingStateChanged(state: State) = Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify about [VoiceBroadcastPlayer.isLiveListening] changes.
|
||||||
|
*/
|
||||||
|
fun onLiveModeChanged(isLive: Boolean) = Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat
|
||||||
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
|
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.sequence
|
|
||||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
||||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -70,18 +69,27 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
|
private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
|
||||||
|
|
||||||
override var currentVoiceBroadcast: VoiceBroadcast? = null
|
override var currentVoiceBroadcast: VoiceBroadcast? = null
|
||||||
|
override var isLiveListening: Boolean = false
|
||||||
|
@MainThread
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
Timber.w("isLiveListening: $field -> $value")
|
||||||
|
field = value
|
||||||
|
onLiveListeningChanged(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var playingState = State.IDLE
|
override var playingState = State.IDLE
|
||||||
@MainThread
|
@MainThread
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field != value) {
|
if (field != value) {
|
||||||
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
Timber.w("playingState: $field -> $value")
|
||||||
field = value
|
field = value
|
||||||
onPlayingStateChanged(value)
|
onPlayingStateChanged(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Map voiceBroadcastId to listeners.*/
|
/** Map voiceBroadcastId to listeners. */
|
||||||
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
||||||
|
|
||||||
override fun playOrResume(voiceBroadcast: VoiceBroadcast) {
|
override fun playOrResume(voiceBroadcast: VoiceBroadcast) {
|
||||||
|
@ -121,7 +129,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run {
|
||||||
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||||
}
|
}
|
||||||
listener.onStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
|
listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE)
|
||||||
|
listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast && isLiveListening)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) {
|
||||||
|
@ -142,7 +151,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
|
|
||||||
private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
|
private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
|
||||||
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
|
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
|
||||||
.onEach { currentVoiceBroadcastEvent = it.getOrNull() }
|
.onEach {
|
||||||
|
currentVoiceBroadcastEvent = it.getOrNull()
|
||||||
|
updateLiveListeningMode()
|
||||||
|
}
|
||||||
.launchIn(sessionScope)
|
.launchIn(sessionScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +202,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
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 ?: run { Timber.w("## VoiceBroadcastPlayer: playlist item has no sequence"); return }
|
val sequence = playlistItem.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 {
|
||||||
|
@ -241,6 +253,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
|
||||||
}
|
}
|
||||||
playingState == State.PLAYING || playingState == State.BUFFERING -> {
|
playingState == State.PLAYING || playingState == State.BUFFERING -> {
|
||||||
|
updateLiveListeningMode(positionMillis)
|
||||||
startPlayback(positionMillis)
|
startPlayback(positionMillis)
|
||||||
}
|
}
|
||||||
playingState == State.IDLE || playingState == State.PAUSED -> {
|
playingState == State.IDLE || playingState == State.PAUSED -> {
|
||||||
|
@ -302,15 +315,57 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPlayingStateChanged(playingState: State) {
|
private fun onPlayingStateChanged(playingState: State) {
|
||||||
// Notify state change to all the listeners attached to the current voice broadcast id
|
// Update live playback flag
|
||||||
|
updateLiveListeningMode()
|
||||||
|
|
||||||
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
||||||
|
// Start or stop playback ticker
|
||||||
when (playingState) {
|
when (playingState) {
|
||||||
State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId)
|
||||||
State.PAUSED,
|
State.PAUSED,
|
||||||
State.BUFFERING,
|
State.BUFFERING,
|
||||||
State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId)
|
||||||
}
|
}
|
||||||
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(playingState) }
|
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||||
|
listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the live listening state according to:
|
||||||
|
* - the voice broadcast state (started/paused/resumed/stopped),
|
||||||
|
* - the playing state (IDLE, PLAYING, PAUSED, BUFFERING),
|
||||||
|
* - the potential seek position (backward/forward).
|
||||||
|
*/
|
||||||
|
private fun updateLiveListeningMode(seekPosition: Int? = null) {
|
||||||
|
isLiveListening = when {
|
||||||
|
// the current voice broadcast is not live (ended)
|
||||||
|
currentVoiceBroadcastEvent?.isLive?.not().orFalse() -> false
|
||||||
|
// the player is stopped or paused
|
||||||
|
playingState == State.IDLE || playingState == State.PAUSED -> false
|
||||||
|
seekPosition != null -> {
|
||||||
|
val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0)
|
||||||
|
val newSequence = playlist.findByPosition(seekPosition)?.sequence
|
||||||
|
// the user has sought forward
|
||||||
|
if (seekDirection >= 0) {
|
||||||
|
// stay in live or latest sequence reached
|
||||||
|
isLiveListening || newSequence == playlist.lastOrNull()?.sequence
|
||||||
|
}
|
||||||
|
// the user has sought backward
|
||||||
|
else {
|
||||||
|
// was in live and stay in the same sequence
|
||||||
|
isLiveListening && newSequence == playlist.currentSequence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise, stay in live or go in live if we reached the latest sequence
|
||||||
|
else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLiveListeningChanged(isLiveListening: Boolean) {
|
||||||
|
currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId ->
|
||||||
|
// Notify live mode change to all the listeners attached to the current voice broadcast id
|
||||||
|
listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ class VoiceBroadcastPlaylist(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findBySequence(sequenceNumber: Int): PlaylistItem? {
|
fun findBySequence(sequenceNumber: Int): PlaylistItem? {
|
||||||
return items.find { it.audioEvent.sequence == sequenceNumber }
|
return items.find { it.sequence == sequenceNumber }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1)
|
fun getNextItem() = findBySequence(currentSequence?.plus(1) ?: 1)
|
||||||
|
@ -64,4 +64,7 @@ class VoiceBroadcastPlaylist(
|
||||||
fun firstOrNull() = findBySequence(1)
|
fun firstOrNull() = findBySequence(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)
|
data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int) {
|
||||||
|
val sequence: Int?
|
||||||
|
get() = audioEvent.sequence
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import im.vector.app.features.voicebroadcast.sequence
|
import im.vector.app.features.voicebroadcast.sequence
|
||||||
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
|
||||||
|
import im.vector.app.features.voicebroadcast.voiceBroadcastId
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
@ -73,14 +74,15 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||||
|
|
||||||
// Observe new timeline events
|
// Observe new timeline events
|
||||||
val listener = object : Timeline.Listener {
|
val listener = object : Timeline.Listener {
|
||||||
private var lastEventId: String? = null
|
private var latestEventId: String? = null
|
||||||
private var lastSequence: Int? = null
|
private var lastSequence: Int? = null
|
||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
val newEvents = lastEventId?.let { eventId -> snapshot.subList(0, snapshot.indexOfFirst { it.eventId == eventId }) } ?: snapshot
|
val latestEventIndex = latestEventId?.let { eventId -> snapshot.indexOfFirst { it.eventId == eventId } }
|
||||||
|
val newEvents = if (latestEventIndex != null) snapshot.subList(0, latestEventIndex) else snapshot
|
||||||
|
|
||||||
// Detect a potential stopped voice broadcast state event
|
// Detect a potential stopped voice broadcast state event
|
||||||
val stopEvent = newEvents.findStopEvent()
|
val stopEvent = newEvents.findStopEvent(voiceBroadcast)
|
||||||
if (stopEvent != null) {
|
if (stopEvent != null) {
|
||||||
lastSequence = stopEvent.content?.lastChunkSequence
|
lastSequence = stopEvent.content?.lastChunkSequence
|
||||||
}
|
}
|
||||||
|
@ -98,7 +100,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
lastEventId = snapshot.firstOrNull()?.eventId
|
latestEventId = snapshot.firstOrNull()?.eventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,8 +119,8 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state.
|
* Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state.
|
||||||
*/
|
*/
|
||||||
private fun List<TimelineEvent>.findStopEvent(): VoiceBroadcastEvent? =
|
private fun List<TimelineEvent>.findStopEvent(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? =
|
||||||
this.mapNotNull { it.root.asVoiceBroadcastEvent() }
|
this.mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeIf { it.voiceBroadcastId == voiceBroadcast.voiceBroadcastId } }
|
||||||
.find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
.find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,11 +21,12 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
|
||||||
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 im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
|
import im.vector.app.features.voicebroadcast.voiceBroadcastId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.events.model.RelationType
|
||||||
|
@ -33,7 +34,7 @@ import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.flow.flow
|
import org.matrix.android.sdk.flow.flow
|
||||||
import org.matrix.android.sdk.flow.unwrap
|
import org.matrix.android.sdk.flow.mapOptional
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -57,10 +58,10 @@ class GetVoiceBroadcastEventUseCase @Inject constructor(
|
||||||
else -> {
|
else -> {
|
||||||
room.flow()
|
room.flow()
|
||||||
.liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty()))
|
.liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty()))
|
||||||
.unwrap()
|
.onStart { emit(latestEvent.root.toOptional()) }
|
||||||
.mapNotNull { it.asVoiceBroadcastEvent() }
|
.distinctUntilChanged()
|
||||||
.filter { it.reference?.eventId == voiceBroadcast.voiceBroadcastId }
|
.filter { !it.hasValue() || it.getOrNull()?.asVoiceBroadcastEvent()?.voiceBroadcastId == voiceBroadcast.voiceBroadcastId }
|
||||||
.map { it.toOptional() }
|
.mapOptional { it.asVoiceBroadcastEvent() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue