diff --git a/changelog.d/7496.wip b/changelog.d/7496.wip
new file mode 100644
index 0000000000..49d15d084f
--- /dev/null
+++ b/changelog.d/7496.wip
@@ -0,0 +1 @@
+[Voice Broadcast] Add seekbar in listening tile
diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt
index 92d28d26c9..07c7b4588f 100644
--- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt
+++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt
@@ -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()
             }
diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt
index e9d311fe03..a4fd8bb4e1 100644
--- a/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt
+++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/timer/CountUpTimer.kt
@@ -66,7 +66,7 @@ class CountUpTimer(private val intervalInMs: Long = 1_000) {
         coroutineScope.cancel()
     }
 
-    interface TickListener {
+    fun interface TickListener {
         fun onTick(milliseconds: Long)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
index 00b9a76de7..0bf70690ba 100644
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
+++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
@@ -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) }
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
index 8c49213a42..faee8f652c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
@@ -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()
         }
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 3f4fae1ce9..ef238d56e6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -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)
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
index bede02c17f..eddfe500b3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
index 13e0477ab6..a7b926f29a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt
@@ -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()
         }
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index 5d9c663210..e4f7bed72f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
index 6937cd3a46..91f27ce5a8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
@@ -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)
 
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
index 7ada0c71f2..0329adf12b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
@@ -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,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt
index fda9a1465f..3e8d6cb487 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt
@@ -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) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
index a2d1e30c99..4b91bbfb0e 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
index e057950790..d3f320db7d 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
index bab7f4c7f9..c108e83e76 100644
--- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
+++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationUserItem.kt
@@ -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()
 
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt
index 48554f51d0..fa8033a211 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastExtensions.kt
@@ -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
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt
index 7864d3b4e3..38fb157748 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt
@@ -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)
     }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
index 2a2a549af0..8c11db4f43 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt
@@ -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.
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index 166e5a12e5..6a6dc6a9e8 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -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)
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt
new file mode 100644
index 0000000000..ff388c2313
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlaylist.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
index 4f9f2de673..d12a329142 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
@@ -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()) {
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt
new file mode 100644
index 0000000000..62207d5b87
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/model/VoiceBroadcast.kt
@@ -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,
+)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt
new file mode 100644
index 0000000000..696d300fc3
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt
@@ -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() }
+            }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt
deleted file mode 100644
index d08fa14a95..0000000000
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastUseCase.kt
+++ /dev/null
@@ -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
-    }
-}
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index bed9407dfa..1d31afba99 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -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"