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_pause_voice_broadcast">Pause voice broadcast</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_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>

View File

@ -74,7 +74,8 @@
<dimen name="location_sharing_live_duration_choice_margin_vertical">22dp</dimen>
<!-- 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 -->
<dimen name="collapsing_toolbar_layout_medium_size">112dp</dimen>

View File

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

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

View File

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

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.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.duration
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
@ -148,4 +149,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
}
fun getDuration(): Int {
return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum()
}
}

View File

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

View File

@ -16,13 +16,17 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.text.format.DateUtils
import android.view.View
import android.widget.ImageButton
import android.widget.SeekBar
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
@ -41,6 +45,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
renderPlayingState(holder, state)
}
player.addListener(voiceBroadcastId, playerListener)
bindSeekBar(holder)
}
override fun renderMetadata(holder: Holder) {
@ -56,28 +61,50 @@ 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(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
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(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
}
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
override fun onStartTrackingTouch(seekBar: SeekBar) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar) {
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.SeekTo(voiceBroadcastId, seekBar.progress))
}
})
}
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
override fun unbind(holder: Holder) {
super.unbind(holder)
player.removeListener(voiceBroadcastId, playerListener)
holder.seekBar.setOnSeekBarChangeListener(null)
}
override fun getViewStubId() = STUB_ID
@ -85,6 +112,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
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 voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
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.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)
fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.playOrResume(roomId, eventId)
fun playOrResumePlayback(roomId: String, voiceBroadcastId: String) = voiceBroadcastPlayer.playOrResume(roomId, voiceBroadcastId)
fun pausePlayback() = voiceBroadcastPlayer.pause()
fun stopPlayback() = voiceBroadcastPlayer.stop()
fun seekTo(voiceBroadcastId: String, positionMillis: Int) {
if (voiceBroadcastPlayer.currentVoiceBroadcastId == voiceBroadcastId) {
voiceBroadcastPlayer.seekTo(positionMillis)
}
}
}

View File

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

View File

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

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_height="wrap_content"
app:barrierDirection="bottom"
app:barrierMargin="12dp"
app:barrierMargin="10dp"
app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/controllerButtonsFlow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:constraint_referenced_ids="playPauseButton,bufferingView"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="10dp"
app:constraint_referenced_ids="fastBackwardButton,playPauseButton,bufferingView,fastForwardButton"
app:layout_constraintBottom_toTopOf="@id/seekBar"
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
android:id="@+id/playPauseButton"
android:layout_width="@dimen/voice_broadcast_controller_button_size"
android:layout_height="@dimen/voice_broadcast_controller_button_size"
android:layout_width="@dimen/voice_broadcast_player_button_size"
android:layout_height="@dimen/voice_broadcast_player_button_size"
android:background="@drawable/bg_rounded_button"
android:backgroundTint="?vctr_system"
android:contentDescription="@string/a11y_play_voice_broadcast"
@ -108,10 +117,43 @@
<ProgressBar
android:id="@+id/bufferingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="@dimen/voice_broadcast_player_button_size"
android:layout_height="@dimen/voice_broadcast_player_button_size"
android:contentDescription="@string/a11y_voice_broadcast_buffering"
android:indeterminate="true"
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>

View File

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