diff --git a/changelog.d/7655.wip b/changelog.d/7655.wip
new file mode 100644
index 0000000000..24358007a9
--- /dev/null
+++ b/changelog.d/7655.wip
@@ -0,0 +1 @@
+Voice Broadcast - Update the buffering display in the timeline
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 307dc8814c..58fc62b347 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3094,12 +3094,13 @@
(%1$s)
Live
+
+ Buffering…
Resume voice broadcast record
Pause voice broadcast record
Stop voice broadcast record
Play or resume voice broadcast
Pause voice broadcast
- Buffering
Fast backward 30 seconds
Fast forward 30 seconds
Can’t start a new voice broadcast
diff --git a/vector/src/main/java/im/vector/app/core/extensions/Flow.kt b/vector/src/main/java/im/vector/app/core/extensions/Flow.kt
new file mode 100644
index 0000000000..82e6e5f9a6
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/extensions/Flow.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.core.extensions
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/**
+ * Returns a flow that invokes the given action after the first value of the upstream flow is emitted downstream.
+ */
+fun Flow.onFirst(action: (T) -> Unit): Flow = flow {
+ var emitted = false
+ collect { value ->
+ emit(value) // always emit value
+
+ if (!emitted) {
+ action(value) // execute the action after the first emission
+ emitted = true
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
index b5ea528bd7..900de041d0 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/AudioMessageHelper.kt
@@ -149,7 +149,7 @@ class AudioMessageHelper @Inject constructor(
}
private fun startPlayback(id: String, file: File) {
- val currentPlaybackTime = playbackTracker.getPlaybackTime(id)
+ val currentPlaybackTime = playbackTracker.getPlaybackTime(id) ?: 0
try {
FileInputStream(file).use { fis ->
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
index 90fd66f9ab..c34cbbc74a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt
@@ -67,8 +67,8 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
fun startPlayback(id: String) {
- val currentPlaybackTime = getPlaybackTime(id)
- val currentPercentage = getPercentage(id)
+ val currentPlaybackTime = getPlaybackTime(id) ?: 0
+ val currentPercentage = getPercentage(id) ?: 0f
val currentState = Listener.State.Playing(currentPlaybackTime, currentPercentage)
setState(id, currentState)
// Pause any active playback
@@ -85,9 +85,10 @@ class AudioMessagePlaybackTracker @Inject constructor() {
}
fun pausePlayback(id: String) {
- if (getPlaybackState(id) is Listener.State.Playing) {
- val currentPlaybackTime = getPlaybackTime(id)
- val currentPercentage = getPercentage(id)
+ val state = getPlaybackState(id)
+ if (state is Listener.State.Playing) {
+ val currentPlaybackTime = state.playbackTime
+ val currentPercentage = state.percentage
setState(id, Listener.State.Paused(currentPlaybackTime, currentPercentage))
}
}
@@ -110,21 +111,23 @@ class AudioMessagePlaybackTracker @Inject constructor() {
fun getPlaybackState(id: String) = states[id]
- fun getPlaybackTime(id: String): Int {
+ fun getPlaybackTime(id: String): Int? {
return when (val state = states[id]) {
is Listener.State.Playing -> state.playbackTime
is Listener.State.Paused -> state.playbackTime
- /* Listener.State.Idle, */
- else -> 0
+ is Listener.State.Recording,
+ Listener.State.Idle,
+ null -> null
}
}
- fun getPercentage(id: String): Float {
+ fun getPercentage(id: String): Float? {
return when (val state = states[id]) {
is Listener.State.Playing -> state.percentage
is Listener.State.Paused -> state.percentage
- /* Listener.State.Idle, */
- else -> 0f
+ is Listener.State.Recording,
+ Listener.State.Idle,
+ null -> null
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
index e5cb677763..38fe1e8f17 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt
@@ -17,7 +17,6 @@
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
@@ -30,6 +29,7 @@ import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAc
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
+import im.vector.app.features.voicebroadcast.views.VoiceBroadcastBufferingView
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
@EpoxyModelClass
@@ -63,10 +63,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
playPauseButton.setOnClickListener {
if (player.currentVoiceBroadcast == voiceBroadcast) {
when (player.playingState) {
- VoiceBroadcastPlayer.State.PLAYING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
+ VoiceBroadcastPlayer.State.PLAYING,
+ VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause)
VoiceBroadcastPlayer.State.PAUSED,
VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
- VoiceBroadcastPlayer.State.BUFFERING -> Unit
}
} else {
callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast))
@@ -86,7 +86,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
override fun renderMetadata(holder: Holder) {
with(holder) {
broadcasterNameMetadata.value = recorderName
- voiceBroadcastMetadata.isVisible = true
listenersCountMetadata.isVisible = false
}
}
@@ -102,10 +101,11 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
with(holder) {
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
- playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
+ voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
when (state) {
- VoiceBroadcastPlayer.State.PLAYING -> {
+ VoiceBroadcastPlayer.State.PLAYING,
+ VoiceBroadcastPlayer.State.BUFFERING -> {
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
}
@@ -114,7 +114,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
}
- VoiceBroadcastPlayer.State.BUFFERING -> Unit
}
renderLiveIndicator(holder)
@@ -142,14 +141,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
renderBackwardForwardButtons(holder, playbackState)
renderLiveIndicator(holder)
if (!isUserSeeking) {
- holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
+ holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
}
}
}
private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) {
val isPlayingOrPaused = playbackState is State.Playing || playbackState is State.Paused
- val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId)
+ val playbackTime = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0
val canBackward = isPlayingOrPaused && playbackTime > 0
val canForward = isPlayingOrPaused && playbackTime < duration
holder.fastBackwardButton.isInvisible = !canBackward
@@ -174,7 +173,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
val playPauseButton by bind(R.id.playPauseButton)
- val bufferingView by bind(R.id.bufferingView)
+ val bufferingView by bind(R.id.bufferingMetadata)
val fastBackwardButton by bind(R.id.fastBackwardButton)
val fastForwardButton by bind(R.id.fastForwardButton)
val seekBar by bind(R.id.seekBar)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
index 724be600a3..f8025d078e 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt
@@ -21,6 +21,7 @@ import android.media.MediaPlayer
import android.media.MediaPlayer.OnPreparedListener
import androidx.annotation.MainThread
import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.extensions.onFirst
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.voice.VoiceFailure
@@ -145,11 +146,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
playingState = State.BUFFERING
observeVoiceBroadcastStateEvent(voiceBroadcast)
- fetchPlaylistAndStartPlayback(voiceBroadcast)
}
private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) {
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
+ .onFirst { fetchPlaylistAndStartPlayback(voiceBroadcast) }
.onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) }
.launchIn(sessionScope)
}
@@ -222,24 +223,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
}
}
- private fun pausePlayback(positionMillis: Int? = null) {
- if (positionMillis == null) {
+ private fun pausePlayback() {
+ playingState = State.PAUSED // This will trigger a playing state update and save the current position
+ if (currentMediaPlayer != null) {
currentMediaPlayer?.pause()
} else {
stopPlayer()
- val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId
- val duration = playlist.duration.takeIf { it > 0 }
- if (voiceBroadcastId != null && duration != null) {
- playbackTracker.updatePausedAtPlaybackTime(voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
- }
}
- playingState = State.PAUSED
}
private fun resumePlayback() {
if (currentMediaPlayer != null) {
- currentMediaPlayer?.start()
playingState = State.PLAYING
+ currentMediaPlayer?.start()
} else {
val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0
startPlayback(savedPosition)
@@ -256,7 +252,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
startPlayback(positionMillis)
}
playingState == State.IDLE || playingState == State.PAUSED -> {
- pausePlayback(positionMillis)
+ stopPlayer()
+ playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration)
}
}
}
@@ -366,8 +363,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
isLiveListening && newSequence == playlist.currentSequence
}
}
- // otherwise, stay in live or go in live if we reached the latest sequence
- else -> isLiveListening || playlist.currentSequence == playlist.lastOrNull()?.sequence
+ // if there is no saved position, go in live
+ getCurrentPlaybackPosition() == null -> true
+ // if we reached the latest sequence, go in live
+ playlist.currentSequence == playlist.lastOrNull()?.sequence -> true
+ // otherwise, do not change
+ else -> isLiveListening
}
}
@@ -392,9 +393,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
}
private fun getCurrentPlaybackPosition(): Int? {
- val playlistPosition = playlist.currentItem?.startTime
- val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition
- val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) }
+ val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null
+ val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) }
+ val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId)
return computedPosition ?: savedPosition
}
@@ -423,17 +424,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// Next media player is already attached to this player and will start playing automatically
if (nextMediaPlayer != null) return
- // Next media player is preparing but not attached yet, reset the currentMediaPlayer and let the new player take over
- if (isPreparingNextPlayer) {
- currentMediaPlayer?.release()
- currentMediaPlayer = null
- playingState = State.BUFFERING
- return
- }
-
- if (!isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
+ val hasEnded = !isLiveListening && mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence
+ if (hasEnded) {
// We'll not receive new chunks anymore so we can stop the live listening
stop()
+ } else {
+ // Enter in buffering mode and release current media player
+ playingState = State.BUFFERING
+ currentMediaPlayer?.release()
+ currentMediaPlayer = null
}
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastBufferingView.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastBufferingView.kt
new file mode 100644
index 0000000000..eabefa323e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastBufferingView.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.voicebroadcast.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import im.vector.app.databinding.ViewVoiceBroadcastBufferingBinding
+
+class VoiceBroadcastBufferingView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr) {
+
+ init {
+ ViewVoiceBroadcastBufferingBinding.inflate(
+ LayoutInflater.from(context),
+ this
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
index e142cb15ce..c743d8a542 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/views/VoiceBroadcastMetadataView.kt
@@ -37,9 +37,9 @@ class VoiceBroadcastMetadataView @JvmOverloads constructor(
)
var value: String
- get() = views.metadataValue.text.toString()
+ get() = views.metadataText.text.toString()
set(newValue) {
- views.metadataValue.text = newValue
+ views.metadataText.text = newValue
}
init {
@@ -61,6 +61,6 @@ class VoiceBroadcastMetadataView @JvmOverloads constructor(
private fun setValue(typedArray: TypedArray) {
val value = typedArray.getString(R.styleable.VoiceBroadcastMetadataView_metadataValue)
- views.metadataValue.text = value
+ views.metadataText.text = value
}
}
diff --git a/vector/src/main/res/drawable/bg_seek_bar.xml b/vector/src/main/res/drawable/bg_seek_bar.xml
index 0a33522dfd..eff461091e 100644
--- a/vector/src/main/res/drawable/bg_seek_bar.xml
+++ b/vector/src/main/res/drawable/bg_seek_bar.xml
@@ -13,9 +13,9 @@
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml
index 2a6fbf5a9e..4c4286af9b 100644
--- a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml
@@ -73,7 +73,7 @@
android:layout_marginTop="12dp"
android:layout_marginBottom="10dp"
android:progressDrawable="@drawable/bg_seek_bar"
- android:thumbTint="?vctr_content_tertiary"
+ android:thumbTint="?vctr_content_secondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/audioPlaybackTime"
app:layout_constraintTop_toBottomOf="@id/audioPlaybackControlButton"
@@ -85,7 +85,7 @@
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textColor="?vctr_content_tertiary"
+ android:textColor="?vctr_content_secondary"
android:layout_marginEnd="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/audioSeekBar"
@@ -104,4 +104,4 @@
android:visibility="gone"
tools:visibility="visible" />
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
index 1d31afba99..3c59d49418 100644
--- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml
@@ -7,7 +7,9 @@
android:layout_height="wrap_content"
android:background="@drawable/rounded_rect_shape_8"
android:backgroundTint="?vctr_content_quinary"
- android:padding="@dimen/layout_vertical_margin">
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:padding="12dp">
+
+
@@ -117,16 +124,6 @@
android:src="@drawable/ic_play_pause_play"
app:tint="?vctr_content_secondary" />
-
-
+ tools:progress="0" />
+ android:padding="12dp">
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/view_voice_broadcast_metadata.xml b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml
index 3bc31cd9a0..70de3e330e 100644
--- a/vector/src/main/res/layout/view_voice_broadcast_metadata.xml
+++ b/vector/src/main/res/layout/view_voice_broadcast_metadata.xml
@@ -18,7 +18,7 @@
tools:src="@drawable/ic_voice_broadcast" />