Merge pull request #7478 from vector-im/feature/fre/voice_broadcast_player_interface

Voice Broadcast - Some internal improvements related to the player
This commit is contained in:
Florian Renaud 2022-10-31 10:55:19 +01:00 committed by GitHub
commit 01ab39ec5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 332 additions and 158 deletions

1
changelog.d/7478.wip Normal file
View File

@ -0,0 +1 @@
[Voice Broadcast] Improve playlist fetching and player codebase

View File

@ -18,24 +18,33 @@ package im.vector.app.core.di
import android.content.Context
import android.os.Build
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorderQ
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object VoiceModule {
@Provides
@Singleton
fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VoiceBroadcastRecorderQ(context)
} else {
null
@Module
abstract class VoiceModule {
companion object {
@Provides
@Singleton
fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VoiceBroadcastRecorderQ(context)
} else {
null
}
}
}
@Binds
abstract fun bindVoiceBroadcastPlayer(player: VoiceBroadcastPlayerImpl): VoiceBroadcastPlayer
}

View File

@ -42,7 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired
import im.vector.app.features.raw.wellknown.withElementWellKnown
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voicebroadcast.usecase.StopOngoingVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.recording.usecase.StopOngoingVoiceBroadcastUseCase
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay

View File

@ -26,11 +26,11 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadca
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem_
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getUserOrDefault

View File

@ -25,9 +25,9 @@ import im.vector.app.R
import im.vector.app.core.extensions.tintBackground
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.util.MatrixItem
abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Holder> : AbsMessageItem<H>() {

View File

@ -23,7 +23,7 @@ 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.voicebroadcast.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
@EpoxyModelClass

View File

@ -22,8 +22,8 @@ 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.VoiceBroadcastAction
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
@EpoxyModelClass

View File

@ -16,10 +16,11 @@
package im.vector.app.features.voicebroadcast
import im.vector.app.features.voicebroadcast.usecase.PauseVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.usecase.ResumeVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.usecase.StartVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.usecase.StopVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase
import javax.inject.Inject
/**

View File

@ -0,0 +1,75 @@
/*
* 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.listening
interface VoiceBroadcastPlayer {
/**
* The current playing voice broadcast identifier, if any.
*/
val currentVoiceBroadcastId: String?
/**
* The current playing [State], [State.IDLE] by default.
*/
val playingState: State
/**
* Start playback of the given voice broadcast.
*/
fun playOrResume(roomId: String, voiceBroadcastId: String)
/**
* Pause playback of the current voice broadcast, if any.
*/
fun pause()
/**
* Stop playback of the current voice broadcast, if any, and reset the player state.
*/
fun stop()
/**
* Add a [Listener] to the given voice broadcast id.
*/
fun addListener(voiceBroadcastId: String, listener: Listener)
/**
* Remove a [Listener] from the given voice broadcast id.
*/
fun removeListener(voiceBroadcastId: String, listener: Listener)
/**
* Player states.
*/
enum class State {
PLAYING,
PAUSED,
BUFFERING,
IDLE
}
/**
* Listener related to [VoiceBroadcastPlayer].
*/
fun interface Listener {
/**
* Notify about [VoiceBroadcastPlayer.playingState] changes.
*/
fun onStateChanged(state: State)
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast
package im.vector.app.features.voicebroadcast.listening
import android.media.AudioAttributes
import android.media.MediaPlayer
@ -22,49 +22,43 @@ 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.listening.VoiceBroadcastPlayer.Listener
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.sequence
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class VoiceBroadcastPlayer @Inject constructor(
class VoiceBroadcastPlayerImpl @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val playbackTracker: AudioMessagePlaybackTracker,
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
) {
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
) : VoiceBroadcastPlayer {
private val session
get() = sessionHolder.getActiveSession()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var voiceBroadcastStateJob: Job? = null
private var currentTimeline: Timeline? = null
set(value) {
field?.removeAllListeners()
field?.dispose()
field = value
}
private val mediaPlayerListener = MediaPlayerListener()
private var timelineListener: TimelineListener? = null
private var currentMediaPlayer: MediaPlayer? = null
private var nextMediaPlayer: MediaPlayer? = null
@ -74,10 +68,13 @@ class VoiceBroadcastPlayer @Inject constructor(
}
private var currentSequence: Int? = null
private var fetchPlaylistJob: Job? = null
private var playlist = emptyList<MessageAudioEvent>()
var currentVoiceBroadcastId: String? = null
private var isLive: Boolean = false
private var state: State = State.IDLE
override var currentVoiceBroadcastId: String? = null
override var playingState = State.IDLE
@MainThread
set(value) {
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
@ -94,25 +91,26 @@ class VoiceBroadcastPlayer @Inject constructor(
*/
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
fun playOrResume(roomId: String, eventId: String) {
val hasChanged = currentVoiceBroadcastId != eventId
override fun playOrResume(roomId: String, voiceBroadcastId: String) {
val hasChanged = currentVoiceBroadcastId != voiceBroadcastId
when {
hasChanged -> startPlayback(roomId, eventId)
state == State.PAUSED -> resumePlayback()
hasChanged -> startPlayback(roomId, voiceBroadcastId)
playingState == State.PAUSED -> resumePlayback()
else -> Unit
}
}
fun pause() {
override fun pause() {
currentMediaPlayer?.pause()
currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) }
state = State.PAUSED
playingState = State.PAUSED
}
fun stop() {
override fun stop() {
// Stop playback
currentMediaPlayer?.stop()
currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) }
isLive = false
// Release current player
release(currentMediaPlayer)
@ -126,58 +124,78 @@ class VoiceBroadcastPlayer @Inject constructor(
voiceBroadcastStateJob?.cancel()
voiceBroadcastStateJob = null
// In case of live broadcast, stop observing new chunks
currentTimeline = null
timelineListener = null
// Do not fetch the playlist anymore
fetchPlaylistJob?.cancel()
fetchPlaylistJob = null
// Update state
state = State.IDLE
playingState = State.IDLE
// Clear playlist
playlist = emptyList()
currentSequence = null
currentRoomId = null
currentVoiceBroadcastId = null
}
/**
* Add a [Listener] to the given voice broadcast id.
*/
fun addListener(voiceBroadcastId: String, listener: Listener) {
override fun addListener(voiceBroadcastId: String, listener: Listener) {
listeners[voiceBroadcastId]?.add(listener) ?: run {
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
}
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(state) else listener.onStateChanged(State.IDLE)
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(playingState) else listener.onStateChanged(State.IDLE)
}
/**
* Remove a [Listener] from the given voice broadcast id.
*/
fun removeListener(voiceBroadcastId: String, listener: Listener) {
override fun removeListener(voiceBroadcastId: String, listener: Listener) {
listeners[voiceBroadcastId]?.remove(listener)
}
private fun startPlayback(roomId: String, eventId: String) {
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
// Stop listening previous voice broadcast if any
if (state != State.IDLE) stop()
if (playingState != State.IDLE) stop()
currentRoomId = roomId
currentVoiceBroadcastId = eventId
state = State.BUFFERING
playingState = State.BUFFERING
val voiceBroadcastState = getVoiceBroadcastUseCase.execute(roomId, eventId)?.content?.voiceBroadcastState
if (voiceBroadcastState == VoiceBroadcastState.STOPPED) {
// Get static playlist
updatePlaylist(getExistingChunks(room, eventId))
startPlayback(false)
} else {
playLiveVoiceBroadcast(room, eventId)
isLive = voiceBroadcastState != null && voiceBroadcastState != VoiceBroadcastState.STOPPED
fetchPlaylistAndStartPlayback(roomId, eventId)
}
private fun fetchPlaylistAndStartPlayback(roomId: String, voiceBroadcastId: String) {
fetchPlaylistJob = getLiveVoiceBroadcastChunksUseCase.execute(roomId, voiceBroadcastId)
.onEach(this::updatePlaylist)
.launchIn(coroutineScope)
}
private fun updatePlaylist(playlist: List<MessageAudioEvent>) {
this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs }
onPlaylistUpdated()
}
private fun onPlaylistUpdated() {
when (playingState) {
State.PLAYING -> {
if (nextMediaPlayer == null) {
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
}
}
State.PAUSED -> {
if (nextMediaPlayer == null) {
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
}
}
State.BUFFERING -> {
val newMediaContent = getNextAudioContent()
if (newMediaContent != null) startPlayback()
}
State.IDLE -> startPlayback()
}
}
private fun startPlayback(isLive: Boolean) {
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
@ -187,7 +205,7 @@ class VoiceBroadcastPlayer @Inject constructor(
currentMediaPlayer?.start()
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
currentSequence = sequence
withContext(Dispatchers.Main) { state = State.PLAYING }
withContext(Dispatchers.Main) { playingState = State.PLAYING }
nextMediaPlayer = prepareNextMediaPlayer()
} catch (failure: Throwable) {
Timber.e(failure, "Unable to start playback")
@ -196,39 +214,15 @@ class VoiceBroadcastPlayer @Inject constructor(
}
}
private fun playLiveVoiceBroadcast(room: Room, eventId: String) {
room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() ?: error("Cannot retrieve voice broadcast $eventId")
updatePlaylist(getExistingChunks(room, eventId))
startPlayback(true)
observeIncomingEvents(room, eventId)
}
private fun getExistingChunks(room: Room, eventId: String): List<MessageAudioEvent> {
return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId)
.mapNotNull { it.root.asMessageAudioEvent() }
.filter { it.isVoiceBroadcast() }
}
private fun observeIncomingEvents(room: Room, eventId: String) {
currentTimeline = room.timelineService().createTimeline(null, TimelineSettings(5)).also { timeline ->
timelineListener = TimelineListener(eventId).also { timeline.addListener(it) }
timeline.start()
}
}
private fun resumePlayback() {
currentMediaPlayer?.start()
currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) }
state = State.PLAYING
}
private fun updatePlaylist(playlist: List<MessageAudioEvent>) {
this.playlist = playlist.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs }
playingState = State.PLAYING
}
private fun getNextAudioContent(): MessageAudioContent? {
val nextSequence = currentSequence?.plus(1)
?: timelineListener?.let { playlist.lastOrNull()?.sequence }
?: playlist.lastOrNull()?.sequence
?: 1
return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content
}
@ -274,37 +268,6 @@ class VoiceBroadcastPlayer @Inject constructor(
}
}
private inner class TimelineListener(private val voiceBroadcastId: String) : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val currentSequences = playlist.map { it.sequence }
val newChunks = snapshot
.mapNotNull { timelineEvent ->
timelineEvent.root.asMessageAudioEvent()
?.takeIf { it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.sequence !in currentSequences }
}
if (newChunks.isEmpty()) return
updatePlaylist(playlist + newChunks)
when (state) {
State.PLAYING -> {
if (nextMediaPlayer == null) {
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
}
}
State.PAUSED -> {
if (nextMediaPlayer == null) {
coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() }
}
}
State.BUFFERING -> {
val newMediaContent = getNextAudioContent()
if (newMediaContent != null) startPlayback(true)
}
State.IDLE -> startPlayback(true)
}
}
}
private inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
override fun onInfo(mp: MediaPlayer, what: Int, extra: Int): Boolean {
@ -324,13 +287,13 @@ class VoiceBroadcastPlayer @Inject constructor(
val roomId = currentRoomId ?: return
val voiceBroadcastId = currentVoiceBroadcastId ?: return
val voiceBroadcastEventContent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)?.content ?: return
val isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED
isLive = voiceBroadcastEventContent.voiceBroadcastState != null && voiceBroadcastEventContent.voiceBroadcastState != VoiceBroadcastState.STOPPED
if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) {
// We'll not receive new chunks anymore so we can stop the live listening
stop()
} else {
state = State.BUFFERING
playingState = State.BUFFERING
}
}
@ -339,15 +302,4 @@ class VoiceBroadcastPlayer @Inject constructor(
return true
}
}
enum class State {
PLAYING,
PAUSED,
BUFFERING,
IDLE
}
fun interface Listener {
fun onStateChanged(state: State)
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.listening.usecase
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.sequence
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastUseCase
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.runningReduce
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import javax.inject.Inject
/**
* Get a [Flow] of [MessageAudioEvent]s related to the given voice broadcast.
*/
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase,
) {
fun execute(roomId: String, voiceBroadcastId: String): Flow<List<MessageAudioEvent>> {
val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
val room = session.roomService().getRoom(roomId) ?: return emptyFlow()
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
// Get initial chunks
val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcastId)
.mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } }
val voiceBroadcastEvent = getVoiceBroadcastUseCase.execute(roomId, voiceBroadcastId)
val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
return if (voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED) {
// Just send the existing chunks if voice broadcast is stopped
flowOf(existingChunks)
} else {
// Observe new timeline events if voice broadcast is ongoing
callbackFlow {
// Init with existing chunks
send(existingChunks)
// Observe new timeline events
val listener = object : Timeline.Listener {
private var lastEventId: String? = null
private var lastSequence: Int? = null
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val newEvents = lastEventId?.let { eventId -> snapshot.subList(0, snapshot.indexOfFirst { it.eventId == eventId }) } ?: snapshot
// Detect a potential stopped voice broadcast state event
val stopEvent = newEvents.findStopEvent()
if (stopEvent != null) {
lastSequence = stopEvent.content?.lastChunkSequence
}
val newChunks = newEvents.mapToChunkEvents(voiceBroadcastId, voiceBroadcastEvent.root.senderId)
// Notify about new chunks
if (newChunks.isNotEmpty()) {
trySend(newChunks)
}
// Automatically stop observing the timeline if the last chunk has been received
if (lastSequence != null && newChunks.any { it.sequence == lastSequence }) {
timeline.removeListener(this)
timeline.dispose()
}
lastEventId = snapshot.firstOrNull()?.eventId
}
}
timeline.addListener(listener)
timeline.start()
awaitClose {
timeline.removeListener(listener)
timeline.dispose()
}
}
.runningReduce { accumulator: List<MessageAudioEvent>, value: List<MessageAudioEvent> -> accumulator.plus(value) }
}
}
/**
* Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state.
*/
private fun List<TimelineEvent>.findStopEvent(): VoiceBroadcastEvent? =
this.mapNotNull { it.root.asVoiceBroadcastEvent() }
.find { it.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
/**
* Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast.
*/
private fun List<TimelineEvent>.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List<MessageAudioEvent> =
this.mapNotNull { timelineEvent ->
timelineEvent.root.asMessageAudioEvent()
?.takeIf {
it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId &&
it.root.senderId == senderId
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast
package im.vector.app.features.voicebroadcast.recording
import androidx.annotation.IntRange
import im.vector.app.features.voice.VoiceRecorder

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast
package im.vector.app.features.voicebroadcast.recording
import android.content.Context
import android.media.MediaRecorder

View File

@ -14,13 +14,13 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast.usecase
package im.vector.app.features.voicebroadcast.recording.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent

View File

@ -14,13 +14,13 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast.usecase
package im.vector.app.features.voicebroadcast.recording.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent

View File

@ -14,17 +14,18 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast.usecase
package im.vector.app.features.voicebroadcast.recording.usecase
import android.content.Context
import androidx.core.content.FileProvider
import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.RelationType

View File

@ -14,11 +14,12 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast.usecase
package im.vector.app.features.voicebroadcast.recording.usecase
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.Membership

View File

@ -14,13 +14,13 @@
* limitations under the License.
*/
package im.vector.app.features.voicebroadcast.usecase
package im.vector.app.features.voicebroadcast.recording.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent

View File

@ -17,9 +17,10 @@
package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.usecase.PauseVoiceBroadcastUseCase
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService
import im.vector.app.test.fakes.FakeSession

View File

@ -17,9 +17,10 @@
package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService
import im.vector.app.test.fakes.FakeSession

View File

@ -17,10 +17,11 @@
package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.usecase.StartVoiceBroadcastUseCase
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService

View File

@ -17,9 +17,10 @@
package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.usecase.StopVoiceBroadcastUseCase
import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService
import im.vector.app.test.fakes.FakeSession