Merge pull request #7419 from vector-im/feature/fre/voice_broadcast_live_listening
Voice broadcast - live listening
This commit is contained in:
		
						commit
						d44d81ed46
					
				
							
								
								
									
										1
									
								
								changelog.d/7419.wip
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								changelog.d/7419.wip
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| [Voice Broadcast] Live listening support | ||||
| @ -401,7 +401,7 @@ fun Event.getRelationContent(): RelationDefaultContent? { | ||||
|             when (getClearType()) { | ||||
|                 EventType.STICKER -> getClearContent().toModel<MessageStickerContent>()?.relatesTo | ||||
|                 in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel<MessageBeaconLocationDataContent>()?.relatesTo | ||||
|                 else -> null | ||||
|                 else -> getClearContent()?.get("m.relates_to")?.toContent().toModel() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -22,6 +22,8 @@ import io.realm.Sort | ||||
| import org.matrix.android.sdk.api.session.events.model.getRelationContent | ||||
| import org.matrix.android.sdk.api.session.events.model.isImageMessage | ||||
| import org.matrix.android.sdk.api.session.events.model.isVideoMessage | ||||
| import org.matrix.android.sdk.api.session.events.model.toModel | ||||
| import org.matrix.android.sdk.api.session.room.model.message.MessageContent | ||||
| import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent | ||||
| import org.matrix.android.sdk.api.util.Optional | ||||
| import org.matrix.android.sdk.internal.database.RealmSessionProvider | ||||
| @ -74,7 +76,13 @@ internal class TimelineEventDataSource @Inject constructor( | ||||
|                     .distinct(TimelineEventEntityFields.EVENT_ID) | ||||
|                     .findAll() | ||||
|                     .mapNotNull { | ||||
|                         timelineEventMapper.map(it).takeIf { it.root.getRelationContent()?.takeIf { it.type == eventType && it.eventId == eventId } != null } | ||||
|                         timelineEventMapper.map(it) | ||||
|                                 .takeIf { | ||||
|                                     val isEventRelatedTo = it.root.getRelationContent()?.takeIf { it.type == eventType && it.eventId == eventId } != null | ||||
|                                     val isContentRelatedTo = it.root.getClearContent()?.toModel<MessageContent>() | ||||
|                                             ?.relatesTo?.takeIf { it.type == eventType && it.eventId == eventId } != null | ||||
|                                     isEventRelatedTo || isContentRelatedTo | ||||
|                                 } | ||||
|                     } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -40,7 +40,7 @@ class VoiceBroadcastHelper @Inject constructor( | ||||
| 
 | ||||
|     suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId) | ||||
| 
 | ||||
|     fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.play(roomId, eventId) | ||||
|     fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.playOrResume(roomId, eventId) | ||||
| 
 | ||||
|     fun pausePlayback() = voiceBroadcastPlayer.pause() | ||||
| 
 | ||||
|  | ||||
| @ -18,14 +18,17 @@ package im.vector.app.features.voicebroadcast | ||||
| 
 | ||||
| import android.media.AudioAttributes | ||||
| import android.media.MediaPlayer | ||||
| import im.vector.app.core.di.ActiveSessionHolder | ||||
| import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker | ||||
| import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State | ||||
| import im.vector.app.features.voice.VoiceFailure | ||||
| import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState | ||||
| import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent | ||||
| 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.launch | ||||
| import org.matrix.android.sdk.api.extensions.orFalse | ||||
| import org.matrix.android.sdk.api.session.Session | ||||
| import org.matrix.android.sdk.api.session.events.model.RelationType | ||||
| import org.matrix.android.sdk.api.session.events.model.getRelationContent | ||||
| import org.matrix.android.sdk.api.session.getRoom | ||||
| @ -33,70 +36,129 @@ 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 javax.inject.Inject | ||||
| import javax.inject.Singleton | ||||
| 
 | ||||
| @Singleton | ||||
| class VoiceBroadcastPlayer @Inject constructor( | ||||
|         private val session: Session, | ||||
|         private val sessionHolder: ActiveSessionHolder, | ||||
|         private val playbackTracker: AudioMessagePlaybackTracker, | ||||
|         private val getVoiceBroadcastUseCase: GetVoiceBroadcastUseCase, | ||||
| ) { | ||||
|     private val session | ||||
|         get() = sessionHolder.getActiveSession() | ||||
| 
 | ||||
|     private val mediaPlayerScope = CoroutineScope(Dispatchers.IO) | ||||
| 
 | ||||
|     private var currentMediaPlayer: MediaPlayer? = null | ||||
|     private var currentPlayingIndex: Int = -1 | ||||
|     private var playlist = emptyList<MessageAudioEvent>() | ||||
|     private val currentVoiceBroadcastEventId | ||||
|         get() = playlist.firstOrNull()?.root?.getRelationContent()?.eventId | ||||
|     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 | ||||
| 
 | ||||
|     fun play(roomId: String, eventId: String) { | ||||
|         val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") | ||||
| 
 | ||||
|         when { | ||||
|             currentVoiceBroadcastEventId != eventId -> { | ||||
|                 stop() | ||||
|                 updatePlaylist(room, eventId) | ||||
|                 startPlayback() | ||||
|     private var currentMediaPlayer: MediaPlayer? = null | ||||
|     private var nextMediaPlayer: MediaPlayer? = null | ||||
|         set(value) { | ||||
|             field = value | ||||
|             currentMediaPlayer?.setNextMediaPlayer(value) | ||||
|         } | ||||
|             playbackTracker.getPlaybackState(eventId) is State.Playing -> pause() | ||||
|             else -> resumePlayback() | ||||
|     private var currentSequence: Int? = null | ||||
| 
 | ||||
|     private var playlist = emptyList<MessageAudioEvent>() | ||||
|     private val currentVoiceBroadcastId | ||||
|         get() = playlist.firstOrNull()?.root?.getRelationContent()?.eventId | ||||
| 
 | ||||
|     private var state: State = State.IDLE | ||||
|         set(value) { | ||||
|             Timber.w("## VoiceBroadcastPlayer state: $field -> $value") | ||||
|             field = value | ||||
|         } | ||||
|     private var currentRoomId: String? = null | ||||
| 
 | ||||
|     fun playOrResume(roomId: String, eventId: String) { | ||||
|         val hasChanged = currentVoiceBroadcastId != eventId | ||||
|         when { | ||||
|             hasChanged -> startPlayback(roomId, eventId) | ||||
|             state == State.PAUSED -> resumePlayback() | ||||
|             else -> Unit | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun pause() { | ||||
|         currentMediaPlayer?.pause() | ||||
|         currentVoiceBroadcastEventId?.let { playbackTracker.pausePlayback(it) } | ||||
|         currentVoiceBroadcastId?.let { playbackTracker.pausePlayback(it) } | ||||
|         state = State.PAUSED | ||||
|     } | ||||
| 
 | ||||
|     fun stop() { | ||||
|         // Stop playback | ||||
|         currentMediaPlayer?.stop() | ||||
|         currentMediaPlayer?.release() | ||||
|         currentMediaPlayer?.setOnInfoListener(null) | ||||
|         currentVoiceBroadcastId?.let { playbackTracker.stopPlayback(it) } | ||||
| 
 | ||||
|         // Release current player | ||||
|         release(currentMediaPlayer) | ||||
|         currentMediaPlayer = null | ||||
|         currentVoiceBroadcastEventId?.let { playbackTracker.stopPlayback(it) } | ||||
| 
 | ||||
|         // Release next player | ||||
|         release(nextMediaPlayer) | ||||
|         nextMediaPlayer = null | ||||
| 
 | ||||
|         // Do not observe anymore voice broadcast state changes | ||||
|         voiceBroadcastStateJob?.cancel() | ||||
|         voiceBroadcastStateJob = null | ||||
| 
 | ||||
|         // In case of live broadcast, stop observing new chunks | ||||
|         currentTimeline = null | ||||
|         timelineListener = null | ||||
| 
 | ||||
|         // Update state | ||||
|         state = State.IDLE | ||||
| 
 | ||||
|         // Clear playlist | ||||
|         playlist = emptyList() | ||||
|         currentPlayingIndex = -1 | ||||
|         currentSequence = null | ||||
|         currentRoomId = null | ||||
|     } | ||||
| 
 | ||||
|     private fun updatePlaylist(room: Room, eventId: String) { | ||||
|         val timelineEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId) | ||||
|         val audioEvents = timelineEvents.mapNotNull { it.root.asMessageAudioEvent() } | ||||
|         playlist = audioEvents.sortedBy { it.getVoiceBroadcastChunk()?.sequence?.toLong() ?: it.root.originServerTs } | ||||
|     private fun startPlayback(roomId: String, eventId: String) { | ||||
|         val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") | ||||
|         currentRoomId = roomId | ||||
| 
 | ||||
|         // Stop listening previous voice broadcast if any | ||||
|         if (state != State.IDLE) stop() | ||||
| 
 | ||||
|         state = 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) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun startPlayback() { | ||||
|         val content = playlist.firstOrNull()?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } | ||||
|         mediaPlayerScope.launch { | ||||
|     private fun startPlayback(isLive: Boolean) { | ||||
|         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 | ||||
|         coroutineScope.launch { | ||||
|             try { | ||||
|                 currentMediaPlayer = prepareMediaPlayer(content) | ||||
|                 currentMediaPlayer?.start() | ||||
|                 currentPlayingIndex = 0 | ||||
|                 currentVoiceBroadcastEventId?.let { playbackTracker.startPlayback(it) } | ||||
|                 prepareNextFile() | ||||
|                 currentVoiceBroadcastId?.let { playbackTracker.startPlayback(it) } | ||||
|                 currentSequence = sequence | ||||
|                 state = State.PLAYING | ||||
|                 nextMediaPlayer = prepareNextMediaPlayer() | ||||
|             } catch (failure: Throwable) { | ||||
|                 Timber.e(failure, "Unable to start playback") | ||||
|                 throw VoiceFailure.UnableToPlay(failure) | ||||
| @ -104,19 +166,46 @@ class VoiceBroadcastPlayer @Inject constructor( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun resumePlayback() { | ||||
|         currentMediaPlayer?.start() | ||||
|         currentVoiceBroadcastEventId?.let { playbackTracker.startPlayback(it) } | ||||
|     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 suspend fun prepareNextFile() { | ||||
|         val nextContent = playlist.getOrNull(currentPlayingIndex + 1)?.content | ||||
|         if (nextContent == null) { | ||||
|             currentMediaPlayer?.setOnCompletionListener(mediaPlayerListener) | ||||
|         } else { | ||||
|             val nextMediaPlayer = prepareMediaPlayer(nextContent) | ||||
|             currentMediaPlayer?.setNextMediaPlayer(nextMediaPlayer) | ||||
|     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 } | ||||
|     } | ||||
| 
 | ||||
|     private fun getNextAudioContent(): MessageAudioContent? { | ||||
|         val nextSequence = currentSequence?.plus(1) | ||||
|                 ?: timelineListener?.let { playlist.lastOrNull()?.sequence } | ||||
|                 ?: 1 | ||||
|         return playlist.find { it.getVoiceBroadcastChunk()?.sequence == nextSequence }?.content | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun prepareNextMediaPlayer(): MediaPlayer? { | ||||
|         val nextContent = getNextAudioContent() ?: return null | ||||
|         return prepareMediaPlayer(nextContent) | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer { | ||||
| @ -140,28 +229,78 @@ class VoiceBroadcastPlayer @Inject constructor( | ||||
|                 setDataSource(fis.fd) | ||||
|                 setOnInfoListener(mediaPlayerListener) | ||||
|                 setOnErrorListener(mediaPlayerListener) | ||||
|                 setOnCompletionListener(mediaPlayerListener) | ||||
|                 prepare() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     inner class MediaPlayerListener : MediaPlayer.OnInfoListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { | ||||
|     private fun release(mp: MediaPlayer?) { | ||||
|         mp?.apply { | ||||
|             release() | ||||
|             setOnInfoListener(null) | ||||
|             setOnCompletionListener(null) | ||||
|             setOnErrorListener(null) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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 { | ||||
|             when (what) { | ||||
|                 MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT -> { | ||||
|                     release(currentMediaPlayer) | ||||
|                     currentMediaPlayer = mp | ||||
|                     currentPlayingIndex++ | ||||
|                     mediaPlayerScope.launch { prepareNextFile() } | ||||
|                     currentSequence = currentSequence?.plus(1) | ||||
|                     coroutineScope.launch { nextMediaPlayer = prepareNextMediaPlayer() } | ||||
|                 } | ||||
|             } | ||||
|             return false | ||||
|         } | ||||
| 
 | ||||
|         override fun onCompletion(mp: MediaPlayer) { | ||||
|             // Verify that a new media has not been set in the mean time | ||||
|             if (!currentMediaPlayer?.isPlaying.orFalse()) { | ||||
|             if (nextMediaPlayer != null) return | ||||
|             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 | ||||
| 
 | ||||
|             if (!isLive && voiceBroadcastEventContent.lastChunkSequence == currentSequence) { | ||||
|                 // We'll not receive new chunks anymore so we can stop the live listening | ||||
|                 stop() | ||||
|             } else { | ||||
|                 state = State.BUFFERING | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -170,4 +309,11 @@ class VoiceBroadcastPlayer @Inject constructor( | ||||
|             return true | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     enum class State { | ||||
|         PLAYING, | ||||
|         PAUSED, | ||||
|         BUFFERING, | ||||
|         IDLE | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -23,6 +23,7 @@ import java.io.File | ||||
| interface VoiceBroadcastRecorder : VoiceRecorder { | ||||
| 
 | ||||
|     var listener: Listener? | ||||
|     var currentSequence: Int | ||||
| 
 | ||||
|     fun startRecord(roomId: String, chunkLength: Int) | ||||
| 
 | ||||
|  | ||||
| @ -21,6 +21,7 @@ import android.media.MediaRecorder | ||||
| import android.os.Build | ||||
| import androidx.annotation.RequiresApi | ||||
| import im.vector.app.features.voice.AbstractVoiceRecorderQ | ||||
| import org.matrix.android.sdk.api.extensions.tryOrNull | ||||
| import org.matrix.android.sdk.api.session.content.ContentAttachmentData | ||||
| 
 | ||||
| @RequiresApi(Build.VERSION_CODES.Q) | ||||
| @ -29,7 +30,8 @@ class VoiceBroadcastRecorderQ( | ||||
| ) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder { | ||||
| 
 | ||||
|     private var maxFileSize = 0L // zero or negative for no limit | ||||
|     private var currentSequence = 0 | ||||
|     private var currentRoomId: String? = null | ||||
|     override var currentSequence = 0 | ||||
| 
 | ||||
|     override var listener: VoiceBroadcastRecorder.Listener? = null | ||||
| 
 | ||||
| @ -51,11 +53,23 @@ class VoiceBroadcastRecorderQ( | ||||
|     } | ||||
| 
 | ||||
|     override fun startRecord(roomId: String, chunkLength: Int) { | ||||
|         currentRoomId = roomId | ||||
|         maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong() | ||||
|         currentSequence = 1 | ||||
|         startRecord(roomId) | ||||
|     } | ||||
| 
 | ||||
|     override fun pauseRecord() { | ||||
|         tryOrNull { mediaRecorder?.stop() } | ||||
|         mediaRecorder?.reset() | ||||
|         notifyOutputFileCreated() | ||||
|     } | ||||
| 
 | ||||
|     override fun resumeRecord() { | ||||
|         currentSequence++ | ||||
|         currentRoomId?.let { startRecord(it) } | ||||
|     } | ||||
| 
 | ||||
|     override fun stopRecord() { | ||||
|         super.stopRecord() | ||||
|         notifyOutputFileCreated() | ||||
|  | ||||
| @ -44,6 +44,8 @@ data class MessageVoiceBroadcastInfoContent( | ||||
|         @Json(name = "state") val voiceBroadcastStateStr: String = "", | ||||
|         /** The length of the voice chunks in seconds. **/ | ||||
|         @Json(name = "chunk_length") val chunkLength: Int? = null, | ||||
|         /** The sequence of the last sent chunk. **/ | ||||
|         @Json(name = "last_chunk_sequence") val lastChunkSequence: Int? = null, | ||||
| ) : MessageContent { | ||||
| 
 | ||||
|     val voiceBroadcastState: VoiceBroadcastState? = VoiceBroadcastState.values() | ||||
|  | ||||
| @ -0,0 +1,40 @@ | ||||
| /* | ||||
|  * 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.usecase | ||||
| 
 | ||||
| import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent | ||||
| import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent | ||||
| import org.matrix.android.sdk.api.session.Session | ||||
| import org.matrix.android.sdk.api.session.events.model.RelationType | ||||
| import org.matrix.android.sdk.api.session.getRoom | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| class GetVoiceBroadcastUseCase @Inject constructor( | ||||
|         private val session: Session, | ||||
| ) { | ||||
| 
 | ||||
|     fun execute(roomId: String, eventId: String): VoiceBroadcastEvent? { | ||||
|         val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId") | ||||
| 
 | ||||
|         Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $eventId") | ||||
| 
 | ||||
|         val initialEvent = room.timelineService().getTimelineEvent(eventId)?.root?.asVoiceBroadcastEvent() // Fallback to initial event | ||||
|         val relatedEvents = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, eventId).sortedBy { it.root.originServerTs } | ||||
|         return relatedEvents.mapNotNull { it.root.asVoiceBroadcastEvent() }.lastOrNull() ?: initialEvent | ||||
|     } | ||||
| } | ||||
| @ -59,6 +59,7 @@ class PauseVoiceBroadcastUseCase @Inject constructor( | ||||
|                 body = MessageVoiceBroadcastInfoContent( | ||||
|                         relatesTo = reference, | ||||
|                         voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value, | ||||
|                         lastChunkSequence = voiceBroadcastRecorder?.currentSequence, | ||||
|                 ).toContent(), | ||||
|         ) | ||||
| 
 | ||||
|  | ||||
| @ -55,7 +55,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( | ||||
|                 QueryStringValue.IsNotEmpty | ||||
|         ) | ||||
|                 .mapNotNull { it.asVoiceBroadcastEvent() } | ||||
|                 .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } | ||||
|                 .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } | ||||
| 
 | ||||
|         if (onGoingVoiceBroadcastEvents.isEmpty()) { | ||||
|             startVoiceBroadcast(room) | ||||
|  | ||||
| @ -60,6 +60,7 @@ class StopVoiceBroadcastUseCase @Inject constructor( | ||||
|                 body = MessageVoiceBroadcastInfoContent( | ||||
|                         relatesTo = reference, | ||||
|                         voiceBroadcastStateStr = VoiceBroadcastState.STOPPED.value, | ||||
|                         lastChunkSequence = voiceBroadcastRecorder?.currentSequence, | ||||
|                 ).toContent(), | ||||
|         ) | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user