Merge pull request #7494 from vector-im/feature/fre/voice_broadcast_seek_to

Voice Broadcast - Add seek bar with basic implementation
This commit is contained in:
Florian Renaud 2022-11-02 23:30:59 +01:00 committed by GitHub
commit f34758c67b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 219 additions and 46 deletions

View File

@ -3094,6 +3094,8 @@
<string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string> <string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string>
<string name="a11y_pause_voice_broadcast">Pause voice broadcast</string> <string name="a11y_pause_voice_broadcast">Pause voice broadcast</string>
<string name="a11y_voice_broadcast_buffering">Buffering</string> <string name="a11y_voice_broadcast_buffering">Buffering</string>
<string name="a11y_voice_broadcast_fast_backward">Fast backward 30 seconds</string>
<string name="a11y_voice_broadcast_fast_forward">Fast forward 30 seconds</string>
<string name="error_voice_broadcast_unauthorized_title">Cant start a new voice broadcast</string> <string name="error_voice_broadcast_unauthorized_title">Cant start a new voice broadcast</string>
<string name="error_voice_broadcast_permission_denied_message">You dont have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string> <string name="error_voice_broadcast_permission_denied_message">You dont have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string> <string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>

View File

@ -74,7 +74,8 @@
<dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen> <dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen>
<!-- Voice Broadcast --> <!-- Voice Broadcast -->
<dimen name="voice_broadcast_controller_button_size">48dp</dimen> <dimen name="voice_broadcast_recorder_button_size">48dp</dimen>
<dimen name="voice_broadcast_player_button_size">36dp</dimen>
<!-- Material 3 --> <!-- Material 3 -->
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen> <dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>

View File

@ -129,9 +129,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
} }
sealed class Listening : VoiceBroadcastAction() { sealed class Listening : VoiceBroadcastAction() {
data class PlayOrResume(val eventId: String) : Listening() data class PlayOrResume(val voiceBroadcastId: String) : Listening()
object Pause : Listening() object Pause : Listening()
object Stop : Listening() object Stop : Listening()
data class SeekTo(val voiceBroadcastId: String, val positionMillis: Int) : Listening()
} }
} }
} }

View File

@ -50,6 +50,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.error.RoomNotFound import im.vector.app.features.home.room.detail.error.RoomNotFound
import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
@ -478,7 +479,7 @@ class TimelineViewModel @AssistedInject constructor(
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.VoiceBroadcastAction -> handleVoiceBroadcastAction(action) is VoiceBroadcastAction -> handleVoiceBroadcastAction(action)
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
@ -620,22 +621,23 @@ class TimelineViewModel @AssistedInject constructor(
} }
} }
private fun handleVoiceBroadcastAction(action: RoomDetailAction.VoiceBroadcastAction) { private fun handleVoiceBroadcastAction(action: VoiceBroadcastAction) {
if (room == null) return if (room == null) return
viewModelScope.launch { viewModelScope.launch {
when (action) { when (action) {
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> { VoiceBroadcastAction.Recording.Start -> {
voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold( voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold(
{ _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) }, { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) },
{ _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) }, { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) },
) )
} }
RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
is RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.eventId) is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.voiceBroadcastId)
RoomDetailAction.VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
RoomDetailAction.VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
is VoiceBroadcastAction.Listening.SeekTo -> voiceBroadcastHelper.seekTo(action.voiceBroadcastId, action.positionMillis)
} }
} }
} }

View File

@ -67,6 +67,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes( val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
voiceBroadcastId = voiceBroadcastId, voiceBroadcastId = voiceBroadcastId,
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
duration = voiceBroadcastEventsGroup.getDuration(),
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
recorder = voiceBroadcastRecorder, recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer, player = voiceBroadcastPlayer,

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.TextUtils
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.duration
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
@ -148,4 +149,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED } return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L } ?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
} }
fun getDuration(): Int {
return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum()
}
} }

View File

@ -94,6 +94,7 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
data class Attributes( data class Attributes(
val voiceBroadcastId: String, val voiceBroadcastId: String,
val voiceBroadcastState: VoiceBroadcastState?, val voiceBroadcastState: VoiceBroadcastState?,
val duration: Int,
val recorderName: String, val recorderName: String,
val recorder: VoiceBroadcastRecorder?, val recorder: VoiceBroadcastRecorder?,
val player: VoiceBroadcastPlayer, val player: VoiceBroadcastPlayer,

View File

@ -16,13 +16,17 @@
package im.vector.app.features.home.room.detail.timeline.item package im.vector.app.features.home.room.detail.timeline.item
import android.text.format.DateUtils
import android.view.View import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
@ -41,6 +45,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
renderPlayingState(holder, state) renderPlayingState(holder, state)
} }
player.addListener(voiceBroadcastId, playerListener) player.addListener(voiceBroadcastId, playerListener)
bindSeekBar(holder)
} }
override fun renderMetadata(holder: Holder) { override fun renderMetadata(holder: Holder) {
@ -56,28 +61,50 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
fastBackwardButton.isInvisible = true
fastForwardButton.isInvisible = true
when (state) { when (state) {
VoiceBroadcastPlayer.State.PLAYING -> { VoiceBroadcastPlayer.State.PLAYING -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) } playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) }
seekBar.isEnabled = true
} }
VoiceBroadcastPlayer.State.IDLE, VoiceBroadcastPlayer.State.IDLE,
VoiceBroadcastPlayer.State.PAUSED -> { VoiceBroadcastPlayer.State.PAUSED -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
playPauseButton.onClick { playPauseButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId)) }
callback?.onTimelineItemAction(RoomDetailAction.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
override fun onStartTrackingTouch(seekBar: SeekBar) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar) {
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress))
}
})
}
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
super.unbind(holder) super.unbind(holder)
player.removeListener(voiceBroadcastId, playerListener) player.removeListener(voiceBroadcastId, playerListener)
holder.seekBar.setOnSeekBarChangeListener(null)
} }
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
@ -85,6 +112,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) { class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
val playPauseButton by bind<ImageButton>(R.id.playPauseButton) val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
val bufferingView by bind<View>(R.id.bufferingView) val bufferingView by bind<View>(R.id.bufferingView)
val fastBackwardButton by bind<ImageButton>(R.id.fastBackwardButton)
val fastForwardButton by bind<ImageButton>(R.id.fastForwardButton)
val seekBar by bind<SeekBar>(R.id.seekBar)
val durationView by bind<TextView>(R.id.playbackDuration)
val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata) val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata) val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata) val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)

View File

@ -32,3 +32,5 @@ fun MessageAudioEvent.getVoiceBroadcastChunk(): VoiceBroadcastChunk? {
} }
val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence val MessageAudioEvent.sequence: Int? get() = getVoiceBroadcastChunk()?.sequence
val MessageAudioEvent.duration get() = content.audioInfo?.duration ?: content.audioWaveformInfo?.duration ?: 0

View File

@ -41,9 +41,15 @@ class VoiceBroadcastHelper @Inject constructor(
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId) suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.playOrResume(roomId, eventId) fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId)
fun pausePlayback() = voiceBroadcastPlayer.pause() fun pausePlayback() = voiceBroadcastPlayer.pause()
fun stopPlayback() = voiceBroadcastPlayer.stop() fun stopPlayback() = voiceBroadcastPlayer.stop()
fun seekTo(voiceBroadcastId: String, positionMillis: Int) {
if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) {
voiceBroadcastPlayer.seekTo(positionMillis)
}
}
} }

View File

@ -43,6 +43,11 @@ interface VoiceBroadcastPlayer {
*/ */
fun stop() fun stop()
/**
* Seek to the given playback position, is milliseconds.
*/
fun seekTo(positionMillis: Int)
/** /**
* Add a [Listener] to the given voice broadcast id. * Add a [Listener] to the given voice broadcast id.
*/ */

View File

@ -22,7 +22,7 @@ import androidx.annotation.MainThread
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.voice.VoiceFailure import im.vector.app.features.voice.VoiceFailure
import im.vector.app.features.voicebroadcast.getVoiceBroadcastChunk import im.vector.app.features.voicebroadcast.duration
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
import timber.log.Timber import timber.log.Timber
@ -62,14 +63,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private var currentMediaPlayer: MediaPlayer? = null private var currentMediaPlayer: MediaPlayer? = null
private var nextMediaPlayer: MediaPlayer? = null private var nextMediaPlayer: MediaPlayer? = null
set(value) {
field = value
currentMediaPlayer?.setNextMediaPlayer(value)
}
private var currentSequence: Int? = null private var currentSequence: Int? = null
private var fetchPlaylistJob: Job? = null private var fetchPlaylistJob: Job? = null
private var playlist = emptyList<MessageAudioEvent>() private var playlist = emptyList<PlaylistItem>()
private var isLive: Boolean = false private var isLive: Boolean = false
override var currentVoiceBroadcastId: String? = null override var currentVoiceBroadcastId: String? = null
@ -170,8 +168,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
.launchIn(coroutineScope) .launchIn(coroutineScope)
} }
private fun updatePlaylist(playlist: List<MessageAudioEvent>) { private fun updatePlaylist(audioEvents: List<MessageAudioEvent>) {
this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } 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() onPlaylistUpdated()
} }
@ -195,16 +203,23 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
} }
} }
private fun startPlayback() { private fun startPlayback(sequence: Int? = null, position: Int = 0) {
val event = if (isLive) playlist.lastOrNull() else playlist.firstOrNull() val playlistItem = when {
val content = event?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } sequence != null -> playlist.find { it.audioEvent.sequence == sequence }
val sequence = event.getVoiceBroadcastChunk()?.sequence isLive -> playlist.lastOrNull()
else -> playlist.firstOrNull()
}
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
val computedSequence = playlistItem.audioEvent.sequence
coroutineScope.launch { coroutineScope.launch {
try { try {
currentMediaPlayer = prepareMediaPlayer(content) currentMediaPlayer = prepareMediaPlayer(content)
currentMediaPlayer?.start() currentMediaPlayer?.start()
if (position > 0) {
currentMediaPlayer?.seekTo(position)
}
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
currentSequence = sequence currentSequence = computedSequence
withContext(Dispatchers.Main) { playingState = State.PLAYING } withContext(Dispatchers.Main) { playingState = State.PLAYING }
nextMediaPlayer = prepareNextMediaPlayer() nextMediaPlayer = prepareNextMediaPlayer()
} catch (failure: Throwable) { } catch (failure: Throwable) {
@ -220,11 +235,27 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
playingState = State.PLAYING playingState = State.PLAYING
} }
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)
}
private fun getNextAudioContent(): MessageAudioContent? { private fun getNextAudioContent(): MessageAudioContent? {
val nextSequence = currentSequence?.plus(1) val nextSequence = currentSequence?.plus(1)
?: playlist.lastOrNull()?.sequence ?: playlist.lastOrNull()?.audioEvent?.sequence
?: 1 ?: 1
return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content return playlist.find { it.audioEvent.sequence == nextSequence }?.audioEvent?.content
} }
private suspend fun prepareNextMediaPlayer(): MediaPlayer? { private suspend fun prepareNextMediaPlayer(): MediaPlayer? {
@ -268,7 +299,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
} }
} }
private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { private inner class MediaPlayerListener :
MediaPlayer.OnInfoListener,
MediaPlayer.OnPreparedListener,
MediaPlayer.OnCompletionListener,
MediaPlayer.OnErrorListener {
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean { override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
when (what) { when (what) {
@ -282,6 +317,17 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
return false 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) { override fun onCompletion(mp: MediaPlayer) {
if (nextMediaPlayer != null) return if (nextMediaPlayer != null) return
val roomId = currentRoomId ?: return val roomId = currentRoomId ?: return
@ -302,4 +348,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
return true return true
} }
} }
private fun getVoiceBroadcastDuration() = playlist.lastOrNull()?.let { it.startTime + it.audioEvent.duration } ?: 0
private data class PlaylistItem(val audioEvent: MessageAudioEvent, val startTime: Int)
} }

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.976,23.15C9.328,23.15 7.054,22.27 5.156,20.511C3.258,18.751 2.207,16.575 2.003,13.982C1.984,13.76 2.054,13.566 2.211,13.399C2.369,13.232 2.568,13.149 2.809,13.149C3.031,13.149 3.221,13.227 3.378,13.385C3.536,13.542 3.633,13.741 3.67,13.982C3.874,16.112 4.762,17.895 6.337,19.33C7.911,20.765 9.791,21.483 11.976,21.483C14.291,21.483 16.259,20.673 17.88,19.052C19.5,17.432 20.311,15.464 20.311,13.149C20.311,10.834 19.524,8.866 17.949,7.245C16.375,5.625 14.43,4.814 12.115,4.814H11.504L12.949,6.259C13.115,6.426 13.199,6.62 13.199,6.842C13.199,7.065 13.115,7.259 12.949,7.426C12.782,7.593 12.587,7.676 12.365,7.676C12.143,7.676 11.948,7.593 11.782,7.426L8.865,4.509C8.772,4.416 8.707,4.324 8.67,4.231C8.633,4.138 8.615,4.037 8.615,3.925C8.615,3.814 8.633,3.712 8.67,3.62C8.707,3.527 8.772,3.435 8.865,3.342L11.81,0.397C11.958,0.249 12.143,0.175 12.365,0.175C12.587,0.175 12.782,0.249 12.949,0.397C13.097,0.564 13.171,0.758 13.171,0.981C13.171,1.203 13.097,1.388 12.949,1.536L11.337,3.148H11.976C13.365,3.148 14.666,3.407 15.88,3.925C17.093,4.444 18.153,5.157 19.06,6.065C19.968,6.972 20.681,8.032 21.2,9.246C21.718,10.459 21.977,11.76 21.977,13.149C21.977,14.538 21.718,15.839 21.2,17.052C20.681,18.265 19.968,19.325 19.06,20.233C18.153,21.14 17.093,21.854 15.88,22.372C14.666,22.891 13.365,23.15 11.976,23.15Z"
android:fillColor="#737D8C"/>
<path
android:pathData="M9.017,17.09C8.557,17.09 8.148,17.011 7.79,16.853C7.434,16.695 7.153,16.476 6.946,16.195C6.739,15.913 6.63,15.588 6.617,15.22H7.819C7.829,15.397 7.888,15.551 7.994,15.683C8.101,15.813 8.243,15.914 8.419,15.987C8.596,16.059 8.794,16.096 9.014,16.096C9.248,16.096 9.456,16.055 9.637,15.974C9.818,15.891 9.96,15.776 10.062,15.629C10.164,15.482 10.215,15.313 10.212,15.121C10.215,14.923 10.163,14.748 10.059,14.597C9.955,14.445 9.803,14.327 9.605,14.242C9.409,14.157 9.173,14.114 8.896,14.114H8.317V13.2H8.896C9.124,13.2 9.323,13.16 9.493,13.082C9.666,13.003 9.801,12.892 9.899,12.749C9.997,12.604 10.045,12.437 10.043,12.248C10.045,12.062 10.004,11.901 9.918,11.765C9.835,11.626 9.717,11.519 9.564,11.442C9.412,11.365 9.234,11.327 9.03,11.327C8.83,11.327 8.644,11.363 8.474,11.436C8.303,11.508 8.166,11.611 8.061,11.746C7.957,11.878 7.902,12.035 7.895,12.219H6.754C6.763,11.852 6.868,11.531 7.071,11.254C7.275,10.974 7.548,10.757 7.889,10.602C8.23,10.444 8.612,10.365 9.036,10.365C9.473,10.365 9.852,10.447 10.174,10.611C10.498,10.773 10.748,10.991 10.925,11.266C11.102,11.541 11.19,11.845 11.19,12.177C11.193,12.546 11.084,12.855 10.864,13.104C10.647,13.353 10.361,13.516 10.008,13.593V13.644C10.468,13.708 10.821,13.879 11.066,14.156C11.313,14.43 11.435,14.772 11.433,15.182C11.433,15.548 11.329,15.876 11.12,16.166C10.913,16.454 10.628,16.679 10.264,16.843C9.901,17.007 9.486,17.09 9.017,17.09ZM14.515,17.125C13.989,17.125 13.537,16.992 13.16,16.725C12.785,16.457 12.496,16.07 12.294,15.565C12.094,15.058 11.993,14.447 11.993,13.734C11.996,13.02 12.097,12.413 12.297,11.912C12.5,11.409 12.788,11.026 13.163,10.761C13.54,10.497 13.991,10.365 14.515,10.365C15.039,10.365 15.49,10.497 15.867,10.761C16.244,11.026 16.533,11.409 16.733,11.912C16.936,12.415 17.037,13.022 17.037,13.734C17.037,14.45 16.936,15.061 16.733,15.568C16.533,16.073 16.244,16.459 15.867,16.725C15.492,16.992 15.041,17.125 14.515,17.125ZM14.515,16.124C14.924,16.124 15.247,15.923 15.483,15.52C15.722,15.115 15.842,14.52 15.842,13.734C15.842,13.214 15.787,12.777 15.679,12.423C15.57,12.07 15.416,11.803 15.218,11.624C15.02,11.443 14.786,11.353 14.515,11.353C14.108,11.353 13.786,11.555 13.55,11.96C13.313,12.363 13.194,12.954 13.192,13.734C13.19,14.256 13.242,14.695 13.349,15.05C13.457,15.406 13.611,15.675 13.809,15.856C14.007,16.035 14.242,16.124 14.515,16.124Z"
android:fillColor="#737D8C"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.001,23.15C14.65,23.15 16.923,22.27 18.822,20.511C20.72,18.751 21.771,16.575 21.975,13.982C21.993,13.76 21.924,13.566 21.766,13.399C21.609,13.232 21.41,13.149 21.169,13.149C20.947,13.149 20.757,13.227 20.6,13.385C20.442,13.542 20.345,13.741 20.308,13.982C20.104,16.112 19.215,17.895 17.641,19.33C16.066,20.765 14.187,21.483 12.001,21.483C9.686,21.483 7.718,20.673 6.098,19.052C4.477,17.432 3.667,15.464 3.667,13.149C3.667,10.834 4.454,8.866 6.028,7.245C7.603,5.625 9.547,4.814 11.862,4.814H12.474L11.029,6.259C10.862,6.426 10.779,6.62 10.779,6.842C10.779,7.065 10.862,7.259 11.029,7.426C11.196,7.593 11.39,7.676 11.612,7.676C11.835,7.676 12.029,7.593 12.196,7.426L15.113,4.509C15.205,4.416 15.27,4.324 15.307,4.231C15.344,4.138 15.363,4.037 15.363,3.925C15.363,3.814 15.344,3.712 15.307,3.62C15.27,3.527 15.205,3.435 15.113,3.342L12.168,0.397C12.02,0.249 11.835,0.175 11.612,0.175C11.39,0.175 11.196,0.249 11.029,0.397C10.881,0.564 10.807,0.758 10.807,0.981C10.807,1.203 10.881,1.388 11.029,1.536L12.64,3.148H12.001C10.612,3.148 9.311,3.407 8.098,3.925C6.885,4.444 5.825,5.157 4.917,6.065C4.01,6.972 3.297,8.032 2.778,9.246C2.259,10.459 2,11.76 2,13.149C2,14.538 2.259,15.839 2.778,17.052C3.297,18.265 4.01,19.325 4.917,20.233C5.825,21.14 6.885,21.854 8.098,22.372C9.311,22.891 10.612,23.15 12.001,23.15Z"
android:fillColor="#737D8C"/>
<path
android:pathData="M9.017,17.09C8.557,17.09 8.148,17.011 7.79,16.853C7.434,16.695 7.153,16.476 6.946,16.195C6.739,15.913 6.63,15.588 6.617,15.22H7.819C7.829,15.397 7.888,15.551 7.994,15.683C8.101,15.813 8.243,15.914 8.419,15.987C8.596,16.059 8.794,16.096 9.014,16.096C9.248,16.096 9.456,16.055 9.637,15.974C9.818,15.891 9.96,15.776 10.062,15.629C10.164,15.482 10.215,15.313 10.212,15.121C10.215,14.923 10.163,14.748 10.059,14.597C9.955,14.445 9.803,14.327 9.605,14.242C9.409,14.157 9.173,14.114 8.896,14.114H8.317V13.2H8.896C9.124,13.2 9.323,13.16 9.493,13.082C9.666,13.003 9.801,12.892 9.899,12.749C9.997,12.604 10.045,12.437 10.043,12.248C10.045,12.062 10.004,11.901 9.918,11.765C9.835,11.626 9.717,11.519 9.564,11.442C9.412,11.365 9.234,11.327 9.03,11.327C8.83,11.327 8.644,11.363 8.474,11.436C8.303,11.508 8.166,11.611 8.061,11.746C7.957,11.878 7.902,12.035 7.895,12.219H6.754C6.763,11.852 6.868,11.531 7.071,11.254C7.275,10.974 7.548,10.757 7.889,10.602C8.23,10.444 8.612,10.365 9.036,10.365C9.473,10.365 9.852,10.447 10.174,10.611C10.498,10.773 10.748,10.991 10.925,11.266C11.102,11.541 11.19,11.845 11.19,12.177C11.193,12.546 11.084,12.855 10.864,13.104C10.647,13.353 10.361,13.516 10.008,13.593V13.644C10.468,13.708 10.821,13.879 11.066,14.156C11.313,14.43 11.435,14.772 11.433,15.182C11.433,15.548 11.329,15.876 11.12,16.166C10.913,16.454 10.628,16.679 10.264,16.843C9.901,17.007 9.486,17.09 9.017,17.09ZM14.515,17.125C13.989,17.125 13.537,16.992 13.16,16.725C12.785,16.457 12.496,16.07 12.294,15.565C12.094,15.058 11.993,14.447 11.993,13.734C11.996,13.02 12.097,12.413 12.297,11.912C12.5,11.409 12.788,11.026 13.163,10.761C13.54,10.497 13.991,10.365 14.515,10.365C15.039,10.365 15.49,10.497 15.867,10.761C16.244,11.026 16.533,11.409 16.733,11.912C16.936,12.415 17.037,13.022 17.037,13.734C17.037,14.45 16.936,15.061 16.733,15.568C16.533,16.073 16.244,16.459 15.867,16.725C15.492,16.992 15.041,17.125 14.515,17.125ZM14.515,16.124C14.924,16.124 15.247,15.923 15.483,15.52C15.722,15.115 15.842,14.52 15.842,13.734C15.842,13.214 15.787,12.777 15.679,12.423C15.57,12.07 15.416,11.803 15.218,11.624C15.02,11.443 14.786,11.353 14.515,11.353C14.108,11.353 13.786,11.555 13.55,11.96C13.313,12.363 13.194,12.954 13.192,13.734C13.19,14.256 13.242,14.695 13.349,15.05C13.457,15.406 13.611,15.675 13.809,15.856C14.007,16.035 14.242,16.124 14.515,16.124Z"
android:fillColor="#737D8C"/>
</vector>

View File

@ -84,22 +84,31 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:barrierMargin="12dp" app:barrierMargin="10dp"
app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" /> app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
<androidx.constraintlayout.helper.widget.Flow <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/controllerButtonsFlow" android:id="@+id/controllerButtonsFlow"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="10dp"
app:constraint_referenced_ids="playPauseButton,bufferingView" app:constraint_referenced_ids="fastBackwardButton,playPauseButton,bufferingView,fastForwardButton"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@id/seekBar"
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" /> app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
<ImageButton
android:id="@+id/fastBackwardButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@android:color/transparent"
android:contentDescription="@string/a11y_voice_broadcast_fast_backward"
android:src="@drawable/ic_player_backward_30"
app:tint="?vctr_content_secondary" />
<ImageButton <ImageButton
android:id="@+id/playPauseButton" android:id="@+id/playPauseButton"
android:layout_width="@dimen/voice_broadcast_controller_button_size" android:layout_width="@dimen/voice_broadcast_player_button_size"
android:layout_height="@dimen/voice_broadcast_controller_button_size" android:layout_height="@dimen/voice_broadcast_player_button_size"
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system" android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_play_voice_broadcast" android:contentDescription="@string/a11y_play_voice_broadcast"
@ -108,10 +117,43 @@
<ProgressBar <ProgressBar
android:id="@+id/bufferingView" android:id="@+id/bufferingView"
android:layout_width="wrap_content" android:layout_width="@dimen/voice_broadcast_player_button_size"
android:layout_height="wrap_content" android:layout_height="@dimen/voice_broadcast_player_button_size"
android:contentDescription="@string/a11y_voice_broadcast_buffering" android:contentDescription="@string/a11y_voice_broadcast_buffering"
android:indeterminate="true" android:indeterminate="true"
android:indeterminateTint="?vctr_content_secondary" /> android:indeterminateTint="?vctr_content_secondary" />
<ImageButton
android:id="@+id/fastForwardButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@android:color/transparent"
android:contentDescription="@string/a11y_voice_broadcast_fast_forward"
android:src="@drawable/ic_player_forward_30"
app:tint="?vctr_content_secondary" />
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:progressDrawable="@drawable/bg_seek_bar"
android:thumbTint="?vctr_content_tertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/playbackDuration"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/controllerButtonsFlow"
tools:progress="40" />
<TextView
android:id="@+id/playbackDuration"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?vctr_content_tertiary"
app:layout_constraintBottom_toBottomOf="@id/seekBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/seekBar"
tools:text="0:23" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -91,8 +91,8 @@
<ImageButton <ImageButton
android:id="@+id/recordButton" android:id="@+id/recordButton"
android:layout_width="@dimen/voice_broadcast_controller_button_size" android:layout_width="@dimen/voice_broadcast_recorder_button_size"
android:layout_height="@dimen/voice_broadcast_controller_button_size" android:layout_height="@dimen/voice_broadcast_recorder_button_size"
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system" android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_resume_voice_broadcast_record" android:contentDescription="@string/a11y_resume_voice_broadcast_record"
@ -100,8 +100,8 @@
<ImageButton <ImageButton
android:id="@+id/stopRecordButton" android:id="@+id/stopRecordButton"
android:layout_width="@dimen/voice_broadcast_controller_button_size" android:layout_width="@dimen/voice_broadcast_recorder_button_size"
android:layout_height="@dimen/voice_broadcast_controller_button_size" android:layout_height="@dimen/voice_broadcast_recorder_button_size"
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system" android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_stop_voice_broadcast_record" android:contentDescription="@string/a11y_stop_voice_broadcast_record"