diff --git a/changelog.d/7478.wip b/changelog.d/7478.wip new file mode 100644 index 0000000000..2e6602b16d --- /dev/null +++ b/changelog.d/7478.wip @@ -0,0 +1 @@ +[Voice Broadcast] Improve playlist fetching and player codebase diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt index 54d556ea91..30a8565771 100644 --- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index c3abdde022..49f2079625 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt index 7a7cb73471..56498fa8d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt index 45f10b68d0..ba9d582ea4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt @@ -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 : AbsMessageItem() { 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 a3e7cc55d5..8df7a9d1a6 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 @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt index e3e86f38e3..17aa1543c0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastRecordingItem.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt index 58e7de7f32..dfc8e35422 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastHelper.kt @@ -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 /** diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt new file mode 100644 index 0000000000..e2870c4011 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt similarity index 63% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 5a04904f69..3999a0e0af 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -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() - 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> = 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().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) { + 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 { - 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) { - 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) { - 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) - } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt new file mode 100644 index 0000000000..8fbd32767d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -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> { + 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) { + 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, value: List -> accumulator.plus(value) } + } + } + + /** + * Find a [VoiceBroadcastEvent] with a [VoiceBroadcastState.STOPPED] state. + */ + private fun List.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.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List = + this.mapNotNull { timelineEvent -> + timelineEvent.root.asMessageAudioEvent() + ?.takeIf { + it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && + it.root.senderId == senderId + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt index 8b69051823..8bc33ed769 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt similarity index 98% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index 5285dc5e3b..519f1f24aa 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt index 1430dd8c86..58e1f26f44 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt index 2f03d4194c..524b64e095 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 2b7ca7b9f1..a1a519a656 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt index ab4d16ab60..791409b869 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt similarity index 95% rename from vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt rename to vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt index bc6a3e7be6..da13100609 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt @@ -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 diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt index 5c42b26c54..a1ec91aab8 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/PauseVoiceBroadcastUseCaseTest.kt @@ -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 diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt index a1bc3a04ec..8b66d45dd4 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt @@ -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 diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 217a395076..59929ef0d7 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -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 diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt index ee6b141bd9..4b15f50be9 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StopVoiceBroadcastUseCaseTest.kt @@ -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