Skip to content

Commit

Permalink
Merge pull request #7655 from vector-im/feature/fre/voice_broadcast_b…
Browse files Browse the repository at this point in the history
…uffering

[Voice Broadcast] Update buffering display and improve playback
  • Loading branch information
Florian14 authored Nov 29, 2022
2 parents 537331c + 1415504 commit 5560694
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 73 deletions.
1 change: 1 addition & 0 deletions changelog.d/7655.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Voice Broadcast - Update the buffering display in the timeline
3 changes: 2 additions & 1 deletion library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3094,12 +3094,13 @@
<string name="audio_message_file_size">(%1$s)</string>

<string name="voice_broadcast_live">Live</string>
<!-- TODO Rename id to voice_broadcast_buffering -->
<string name="a11y_voice_broadcast_buffering">Buffering…</string>
<string name="a11y_resume_voice_broadcast_record">Resume voice broadcast record</string>
<string name="a11y_pause_voice_broadcast_record">Pause voice broadcast record</string>
<string name="a11y_stop_voice_broadcast_record">Stop voice broadcast record</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_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>
Expand Down
35 changes: 35 additions & 0 deletions vector/src/main/java/im/vector/app/core/extensions/Flow.kt
Original file line number Diff line number Diff line change
@@ -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 <T> Flow<T>.onFirst(action: (T) -> Unit): Flow<T> = flow {
var emitted = false
collect { value ->
emit(value) // always emit value

if (!emitted) {
action(value) // execute the action after the first emission
emitted = true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
}
}
Expand All @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -86,7 +86,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
override fun renderMetadata(holder: Holder) {
with(holder) {
broadcasterNameMetadata.value = recorderName
voiceBroadcastMetadata.isVisible = true
listenersCountMetadata.isVisible = false
}
}
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -174,7 +173,7 @@ 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 bufferingView by bind<VoiceBroadcastBufferingView>(R.id.bufferingMetadata)
val fastBackwardButton by bind<ImageButton>(R.id.fastBackwardButton)
val fastForwardButton by bind<ImageButton>(R.id.fastForwardButton)
val seekBar by bind<SeekBar>(R.id.seekBar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -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
}
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}
4 changes: 2 additions & 2 deletions vector/src/main/res/drawable/bg_seek_bar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<shape
android:shape="line">
<stroke
android:color="?vctr_content_tertiary"
android:color="?vctr_content_secondary"
android:width="2dp"/>
</shape>
</clip>
</item>
</layer-list>
</layer-list>
Loading

0 comments on commit 5560694

Please sign in to comment.