mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
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:
commit
f34758c67b
16 changed files with 219 additions and 46 deletions
|
@ -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">Can’t start a new voice broadcast</string>
|
||||
<string name="error_voice_broadcast_permission_denied_message">You don’t 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
12
vector/src/main/res/drawable/ic_player_backward_30.xml
Normal file
12
vector/src/main/res/drawable/ic_player_backward_30.xml
Normal 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>
|
12
vector/src/main/res/drawable/ic_player_forward_30.xml
Normal file
12
vector/src/main/res/drawable/ic_player_forward_30.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue