Merge pull request #7387 from vector-im/feature/fre/voice_broadcast_start_listening
Voice Broadcast - Listening
This commit is contained in:
commit
0dad78a24a
1
changelog.d/7387.wip
Normal file
1
changelog.d/7387.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Start listening to a voice broadcast
|
@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|||||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
|
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
@ -357,6 +358,10 @@ fun Event.isAudioMessage(): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.isVoiceMessage(): Boolean {
|
||||||
|
return this.asMessageAudioEvent()?.content?.voiceMessageIndicator != null
|
||||||
|
}
|
||||||
|
|
||||||
fun Event.isFileMessage(): Boolean {
|
fun Event.isFileMessage(): Boolean {
|
||||||
return when (getMsgType()) {
|
return when (getMsgType()) {
|
||||||
MessageType.MSGTYPE_FILE -> true
|
MessageType.MSGTYPE_FILE -> true
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.model.message
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Event] wrapper for [EventType.MESSAGE] event type.
|
||||||
|
* Provides additional fields and functions related to this event type.
|
||||||
|
*/
|
||||||
|
@JvmInline
|
||||||
|
value class MessageAudioEvent(val root: Event) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mapped [MessageAudioContent] model of the event content.
|
||||||
|
*/
|
||||||
|
val content: MessageAudioContent
|
||||||
|
get() = root.getClearContent().toModel<MessageContent>() as MessageAudioContent
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(tryOrNull { content } != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a [EventType.MESSAGE] event to a [MessageAudioEvent].
|
||||||
|
*/
|
||||||
|
fun Event.asMessageAudioEvent() = if (getClearType() == EventType.MESSAGE) {
|
||||||
|
tryOrNull { MessageAudioEvent(this) }
|
||||||
|
} else null
|
@ -55,4 +55,9 @@ interface TimelineService {
|
|||||||
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
|
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
|
||||||
*/
|
*/
|
||||||
fun getAttachmentMessages(): List<TimelineEvent>
|
fun getAttachmentMessages(): List<TimelineEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot list of TimelineEvent with a content relation of the given type to the given eventId.
|
||||||
|
*/
|
||||||
|
fun getTimelineEventsRelatedTo(relationType: String, eventId: String): List<TimelineEvent>
|
||||||
}
|
}
|
||||||
|
@ -96,4 +96,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||||||
override fun getAttachmentMessages(): List<TimelineEvent> {
|
override fun getAttachmentMessages(): List<TimelineEvent> {
|
||||||
return timelineEventDataSource.getAttachmentMessages(roomId)
|
return timelineEventDataSource.getAttachmentMessages(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTimelineEventsRelatedTo(relationType: String, eventId: String): List<TimelineEvent> {
|
||||||
|
return timelineEventDataSource.getTimelineEventsRelatedTo(roomId, relationType, eventId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import io.realm.Sort
|
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.isImageMessage
|
||||||
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
|
import org.matrix.android.sdk.api.session.events.model.isVideoMessage
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
@ -63,4 +64,18 @@ internal class TimelineEventDataSource @Inject constructor(
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTimelineEventsRelatedTo(roomId: String, eventType: String, eventId: String): List<TimelineEvent> {
|
||||||
|
// TODO Remove this trick and call relations API
|
||||||
|
// see https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1roomsroomidrelationseventidreltypeeventtype
|
||||||
|
return realmSessionProvider.withRealm { realm ->
|
||||||
|
TimelineEventEntity.whereRoomId(realm, roomId)
|
||||||
|
.sort(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, Sort.ASCENDING)
|
||||||
|
.distinct(TimelineEventEntityFields.EVENT_ID)
|
||||||
|
.findAll()
|
||||||
|
.mapNotNull {
|
||||||
|
timelineEventMapper.map(it).takeIf { it.root.getRelationContent()?.takeIf { it.type == eventType && it.eventId == eventId } != null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,9 +121,17 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||||||
object OpenElementCallWidget : RoomDetailAction()
|
object OpenElementCallWidget : RoomDetailAction()
|
||||||
|
|
||||||
sealed class VoiceBroadcastAction : RoomDetailAction() {
|
sealed class VoiceBroadcastAction : RoomDetailAction() {
|
||||||
object Start : VoiceBroadcastAction()
|
sealed class Recording : VoiceBroadcastAction() {
|
||||||
object Pause : VoiceBroadcastAction()
|
object Start : Recording()
|
||||||
object Resume : VoiceBroadcastAction()
|
object Pause : Recording()
|
||||||
object Stop : VoiceBroadcastAction()
|
object Resume : Recording()
|
||||||
|
object Stop : Recording()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Listening : VoiceBroadcastAction() {
|
||||||
|
data class PlayOrResume(val eventId: String) : Listening()
|
||||||
|
object Pause : Listening()
|
||||||
|
object Stop : Listening()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -604,10 +604,13 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
if (room == null) return
|
if (room == null) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
RoomDetailAction.VoiceBroadcastAction.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId)
|
||||||
RoomDetailAction.VoiceBroadcastAction.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
|
||||||
RoomDetailAction.VoiceBroadcastAction.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
|
||||||
RoomDetailAction.VoiceBroadcastAction.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
|
||||||
|
is RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(room.roomId, action.eventId)
|
||||||
|
RoomDetailAction.VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback()
|
||||||
|
RoomDetailAction.VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,8 +234,9 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
// TODO remove this when there will be a recording indicator outside of the timeline
|
// TODO remove this when there will be a recording indicator outside of the timeline
|
||||||
// Pause voice broadcast if the timeline is not shown anymore
|
// Pause voice broadcast if the timeline is not shown anymore
|
||||||
it.isVoiceBroadcasting && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Pause)
|
it.isVoiceBroadcasting && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause)
|
||||||
else -> {
|
else -> {
|
||||||
|
timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause)
|
||||||
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -684,7 +685,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
locationOwnerId = session.myUserId
|
locationOwnerId = session.myUserId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Start)
|
AttachmentTypeSelectorView.Type.VOICE_BROADCAST -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Start)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
|||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -84,6 +85,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
private val rainbowGenerator: RainbowGenerator,
|
private val rainbowGenerator: RainbowGenerator,
|
||||||
private val audioMessageHelper: AudioMessageHelper,
|
private val audioMessageHelper: AudioMessageHelper,
|
||||||
private val analyticsTracker: AnalyticsTracker,
|
private val analyticsTracker: AnalyticsTracker,
|
||||||
|
private val voiceBroadcastHelper: VoiceBroadcastHelper,
|
||||||
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
|
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
|
||||||
|
|
||||||
private val room = session.getRoom(initialState.roomId)!!
|
private val room = session.getRoom(initialState.roomId)!!
|
||||||
@ -981,6 +983,8 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
private fun handleEntersBackground(composerText: String) {
|
private fun handleEntersBackground(composerText: String) {
|
||||||
// Always stop all voice actions. It may be playing in timeline or active recording
|
// Always stop all voice actions. It may be playing in timeline or active recording
|
||||||
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
|
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
|
||||||
|
// TODO remove this when there will be a listening indicator outside of the timeline
|
||||||
|
voiceBroadcastHelper.pausePlayback()
|
||||||
|
|
||||||
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
|
val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording }
|
||||||
if (isVoiceRecording) {
|
if (isVoiceRecording) {
|
||||||
|
@ -43,9 +43,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStat
|
|||||||
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem
|
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem_
|
||||||
@ -58,8 +56,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem
|
|||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem_
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.PollItem
|
import im.vector.app.features.home.room.detail.timeline.item.PollItem
|
||||||
@ -82,8 +78,8 @@ import im.vector.app.features.media.ImageContentRenderer
|
|||||||
import im.vector.app.features.media.VideoContentRenderer
|
import im.vector.app.features.media.VideoContentRenderer
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.voice.AudioWaveformView
|
import im.vector.app.features.voice.AudioWaveformView
|
||||||
|
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||||
@ -107,6 +103,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
|||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
||||||
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
|
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
|
||||||
@ -141,6 +138,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
private val urlMapProvider: UrlMapProvider,
|
private val urlMapProvider: UrlMapProvider,
|
||||||
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
|
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
|
||||||
private val pollItemViewStateFactory: PollItemViewStateFactory,
|
private val pollItemViewStateFactory: PollItemViewStateFactory,
|
||||||
|
private val voiceBroadcastItemFactory: VoiceBroadcastItemFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO inject this properly?
|
// TODO inject this properly?
|
||||||
@ -203,7 +201,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
||||||
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
||||||
is MessageVoiceBroadcastInfoContent -> buildVoiceBroadcastItem(messageContent, params.eventsGroup, highlight, callback, attributes)
|
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(messageContent, params.eventsGroup, highlight, callback, attributes)
|
||||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
return messageItem?.apply {
|
return messageItem?.apply {
|
||||||
@ -323,7 +321,10 @@ class MessageItemFactory @Inject constructor(
|
|||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
attributes: AbsMessageItem.Attributes
|
attributes: AbsMessageItem.Attributes
|
||||||
): MessageVoiceItem {
|
): MessageVoiceItem? {
|
||||||
|
// Do not display voice broadcast messages
|
||||||
|
if (params.event.root.asMessageAudioEvent().isVoiceBroadcast()) return null
|
||||||
|
|
||||||
val fileUrl = getAudioFileUrl(messageContent, informationData)
|
val fileUrl = getAudioFileUrl(messageContent, informationData)
|
||||||
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
|
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
|
||||||
|
|
||||||
@ -713,25 +714,6 @@ class MessageItemFactory @Inject constructor(
|
|||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildVoiceBroadcastItem(
|
|
||||||
messageContent: MessageVoiceBroadcastInfoContent,
|
|
||||||
eventsGroup: TimelineEventsGroup?,
|
|
||||||
highlight: Boolean,
|
|
||||||
callback: TimelineEventController.Callback?,
|
|
||||||
attributes: AbsMessageItem.Attributes,
|
|
||||||
): MessageVoiceBroadcastItem? {
|
|
||||||
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
|
|
||||||
val voiceBroadcastEventsGroup = eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
|
||||||
val mostRecentEvent = voiceBroadcastEventsGroup.getLastEvent()
|
|
||||||
val mostRecentMessageContent = (mostRecentEvent.getVectorLastMessageContent() as? MessageVoiceBroadcastInfoContent) ?: return null
|
|
||||||
return MessageVoiceBroadcastItem_()
|
|
||||||
.attributes(attributes)
|
|
||||||
.highlighted(highlight)
|
|
||||||
.voiceBroadcastState(mostRecentMessageContent.voiceBroadcastState)
|
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
|
||||||
.callback(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<Int?>?.toFft(): List<Int>? {
|
private fun List<Int?>?.toFft(): List<Int>? {
|
||||||
return this
|
return this
|
||||||
?.filterNotNull()
|
?.filterNotNull()
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastItem_
|
||||||
|
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 org.matrix.android.sdk.api.session.Session
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VoiceBroadcastItemFactory @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val avatarSizeProvider: AvatarSizeProvider,
|
||||||
|
private val audioMessagePlaybackTracker: AudioMessagePlaybackTracker,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
messageContent: MessageVoiceBroadcastInfoContent,
|
||||||
|
eventsGroup: TimelineEventsGroup?,
|
||||||
|
highlight: Boolean,
|
||||||
|
callback: TimelineEventController.Callback?,
|
||||||
|
attributes: AbsMessageItem.Attributes,
|
||||||
|
): MessageVoiceBroadcastItem? {
|
||||||
|
// Only display item of the initial event with updated data
|
||||||
|
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
|
||||||
|
val voiceBroadcastEventsGroup = eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
||||||
|
val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent()
|
||||||
|
val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent()
|
||||||
|
val mostRecentMessageContent = mostRecentEvent?.content ?: return null
|
||||||
|
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
|
||||||
|
return MessageVoiceBroadcastItem_()
|
||||||
|
.attributes(attributes)
|
||||||
|
.highlighted(highlight)
|
||||||
|
.voiceBroadcastState(mostRecentMessageContent.voiceBroadcastState)
|
||||||
|
.recording(isRecording)
|
||||||
|
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
|
||||||
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
.callback(callback)
|
||||||
|
}
|
||||||
|
}
|
@ -18,12 +18,15 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
|||||||
|
|
||||||
import im.vector.app.core.utils.TextUtils
|
import im.vector.app.core.utils.TextUtils
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||||
|
import im.vector.app.features.voicebroadcast.getVoiceBroadcastEventId
|
||||||
|
import im.vector.app.features.voicebroadcast.isVoiceBroadcast
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
|
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
|
||||||
import org.threeten.bp.Duration
|
import org.threeten.bp.Duration
|
||||||
@ -61,6 +64,10 @@ class TimelineEventsGroups {
|
|||||||
EventType.isCallEvent(type) -> (content?.get("call_id") as? String)
|
EventType.isCallEvent(type) -> (content?.get("call_id") as? String)
|
||||||
type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> root.asVoiceBroadcastEvent()?.reference?.eventId
|
type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> root.asVoiceBroadcastEvent()?.reference?.eventId
|
||||||
type == EventType.STATE_ROOM_WIDGET || type == EventType.STATE_ROOM_WIDGET_LEGACY -> root.stateKey
|
type == EventType.STATE_ROOM_WIDGET || type == EventType.STATE_ROOM_WIDGET_LEGACY -> root.stateKey
|
||||||
|
type == EventType.MESSAGE && root.asMessageAudioEvent().isVoiceBroadcast() -> {
|
||||||
|
// Group voice messages with a reference to an eventId
|
||||||
|
root.asMessageAudioEvent()?.getVoiceBroadcastEventId()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@ -134,8 +141,8 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
|
class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
|
||||||
fun getLastEvent(): TimelineEvent {
|
fun getLastDisplayableEvent(): TimelineEvent {
|
||||||
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
||||||
?: group.events.maxBy { it.root.originServerTs ?: 0L }
|
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,10 @@ import android.widget.TextView
|
|||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
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.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
|
||||||
@EpoxyModelClass
|
@EpoxyModelClass
|
||||||
@ -35,6 +37,15 @@ abstract class MessageVoiceBroadcastItem : AbsMessageItem<MessageVoiceBroadcastI
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var voiceBroadcastState: VoiceBroadcastState? = null
|
var voiceBroadcastState: VoiceBroadcastState? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var recording: Boolean = false
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
|
||||||
|
|
||||||
|
private val voiceBroadcastEventId
|
||||||
|
get() = attributes.informationData.eventId
|
||||||
|
|
||||||
override fun isCacheable(): Boolean = false
|
override fun isCacheable(): Boolean = false
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
@ -44,16 +55,37 @@ abstract class MessageVoiceBroadcastItem : AbsMessageItem<MessageVoiceBroadcastI
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n") // Temporary text
|
@SuppressLint("SetTextI18n") // Temporary text
|
||||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||||
|
holder.currentStateText.text = "Voice Broadcast state: ${voiceBroadcastState?.value ?: "None"}"
|
||||||
|
if (recording) {
|
||||||
|
renderRecording(holder)
|
||||||
|
} else {
|
||||||
|
renderListening(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderListening(holder: Holder) {
|
||||||
|
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
|
||||||
|
override fun onUpdate(state: State) {
|
||||||
|
holder.playButton.isEnabled = state !is State.Playing
|
||||||
|
holder.pauseButton.isEnabled = state is State.Playing
|
||||||
|
holder.stopButton.isEnabled = state !is State.Idle
|
||||||
|
}
|
||||||
|
})
|
||||||
|
holder.playButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastEventId)) }
|
||||||
|
holder.pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) }
|
||||||
|
holder.stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Stop) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderRecording(holder: Holder) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
currentStateText.text = "Voice Broadcast state: ${voiceBroadcastState?.value ?: "None"}"
|
|
||||||
playButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.PAUSED
|
playButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.PAUSED
|
||||||
pauseButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED || voiceBroadcastState == VoiceBroadcastState.RESUMED
|
pauseButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED || voiceBroadcastState == VoiceBroadcastState.RESUMED
|
||||||
stopButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED ||
|
stopButton.isEnabled = voiceBroadcastState == VoiceBroadcastState.STARTED ||
|
||||||
voiceBroadcastState == VoiceBroadcastState.RESUMED ||
|
voiceBroadcastState == VoiceBroadcastState.RESUMED ||
|
||||||
voiceBroadcastState == VoiceBroadcastState.PAUSED
|
voiceBroadcastState == VoiceBroadcastState.PAUSED
|
||||||
playButton.setOnClickListener { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Resume) }
|
playButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
|
||||||
pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Pause) }
|
pauseButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
|
||||||
stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Stop) }
|
stopButton.setOnClickListener { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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.room.model.message.MessageAudioEvent
|
||||||
|
|
||||||
|
fun MessageAudioEvent?.isVoiceBroadcast() = this?.getVoiceBroadcastEventId() != null
|
||||||
|
|
||||||
|
fun MessageAudioEvent.getVoiceBroadcastEventId(): String? =
|
||||||
|
// TODO Improve this condition by checking the referenced event type
|
||||||
|
root.takeIf { content.voiceMessageIndicator != null }
|
||||||
|
?.getRelationContent()?.takeIf { it.type == RelationType.REFERENCE }
|
||||||
|
?.eventId
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 New Vector Ltd
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -30,6 +30,7 @@ class VoiceBroadcastHelper @Inject constructor(
|
|||||||
private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase,
|
private val pauseVoiceBroadcastUseCase: PauseVoiceBroadcastUseCase,
|
||||||
private val resumeVoiceBroadcastUseCase: ResumeVoiceBroadcastUseCase,
|
private val resumeVoiceBroadcastUseCase: ResumeVoiceBroadcastUseCase,
|
||||||
private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
|
private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
|
||||||
|
private val voiceBroadcastPlayer: VoiceBroadcastPlayer,
|
||||||
) {
|
) {
|
||||||
suspend fun startVoiceBroadcast(roomId: String) = startVoiceBroadcastUseCase.execute(roomId)
|
suspend fun startVoiceBroadcast(roomId: String) = startVoiceBroadcastUseCase.execute(roomId)
|
||||||
|
|
||||||
@ -38,4 +39,10 @@ class VoiceBroadcastHelper @Inject constructor(
|
|||||||
suspend fun resumeVoiceBroadcast(roomId: String) = resumeVoiceBroadcastUseCase.execute(roomId)
|
suspend fun resumeVoiceBroadcast(roomId: String) = resumeVoiceBroadcastUseCase.execute(roomId)
|
||||||
|
|
||||||
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
|
suspend fun stopVoiceBroadcast(roomId: String) = stopVoiceBroadcastUseCase.execute(roomId)
|
||||||
|
|
||||||
|
fun playOrResumePlayback(roomId: String, eventId: String) = voiceBroadcastPlayer.play(roomId, eventId)
|
||||||
|
|
||||||
|
fun pausePlayback() = voiceBroadcastPlayer.pause()
|
||||||
|
|
||||||
|
fun stopPlayback() = voiceBroadcastPlayer.stop()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
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 kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
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
|
||||||
|
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 timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class VoiceBroadcastPlayer @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val playbackTracker: AudioMessagePlaybackTracker,
|
||||||
|
) {
|
||||||
|
|
||||||
|
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 mediaPlayerListener = MediaPlayerListener()
|
||||||
|
|
||||||
|
fun play(roomId: String, eventId: String) {
|
||||||
|
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")
|
||||||
|
|
||||||
|
when {
|
||||||
|
currentVoiceBroadcastEventId != eventId -> {
|
||||||
|
stop()
|
||||||
|
updatePlaylist(room, eventId)
|
||||||
|
startPlayback()
|
||||||
|
}
|
||||||
|
playbackTracker.getPlaybackState(eventId) is State.Playing -> pause()
|
||||||
|
else -> resumePlayback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pause() {
|
||||||
|
currentMediaPlayer?.pause()
|
||||||
|
currentVoiceBroadcastEventId?.let { playbackTracker.pausePlayback(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
currentMediaPlayer?.stop()
|
||||||
|
currentMediaPlayer?.release()
|
||||||
|
currentMediaPlayer?.setOnInfoListener(null)
|
||||||
|
currentMediaPlayer = null
|
||||||
|
currentVoiceBroadcastEventId?.let { playbackTracker.stopPlayback(it) }
|
||||||
|
playlist = emptyList()
|
||||||
|
currentPlayingIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
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.root.originServerTs }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPlayback() {
|
||||||
|
val content = playlist.firstOrNull()?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
|
||||||
|
mediaPlayerScope.launch {
|
||||||
|
try {
|
||||||
|
currentMediaPlayer = prepareMediaPlayer(content)
|
||||||
|
currentMediaPlayer?.start()
|
||||||
|
currentPlayingIndex = 0
|
||||||
|
currentVoiceBroadcastEventId?.let { playbackTracker.startPlayback(it) }
|
||||||
|
prepareNextFile()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "Unable to start playback")
|
||||||
|
throw VoiceFailure.UnableToPlay(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resumePlayback() {
|
||||||
|
currentMediaPlayer?.start()
|
||||||
|
currentVoiceBroadcastEventId?.let { playbackTracker.startPlayback(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 suspend fun prepareMediaPlayer(messageAudioContent: MessageAudioContent): MediaPlayer {
|
||||||
|
// Download can fail
|
||||||
|
val audioFile = try {
|
||||||
|
session.fileService().downloadFile(messageAudioContent)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "Unable to start playback")
|
||||||
|
throw VoiceFailure.UnableToPlay(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioFile.inputStream().use { fis ->
|
||||||
|
MediaPlayer().apply {
|
||||||
|
setAudioAttributes(
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
// Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||||
|
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
setDataSource(fis.fd)
|
||||||
|
setOnInfoListener(mediaPlayerListener)
|
||||||
|
setOnErrorListener(mediaPlayerListener)
|
||||||
|
prepare()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 -> {
|
||||||
|
currentMediaPlayer = mp
|
||||||
|
currentPlayingIndex++
|
||||||
|
mediaPlayerScope.launch { prepareNextFile() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCompletion(mp: MediaPlayer) {
|
||||||
|
// Verify that a new media has not been set in the mean time
|
||||||
|
if (!currentMediaPlayer?.isPlaying.orFalse()) {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||||
|
stop()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user