Update voice broadcast recorder according to the most recent voice broadcast state event

This commit is contained in:
Florian Renaud 2022-11-23 17:31:29 +01:00
parent f436de1230
commit 763b60ee6b
7 changed files with 78 additions and 36 deletions

View file

@ -27,6 +27,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
import javax.inject.Singleton import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -36,9 +37,17 @@ abstract class VoiceModule {
companion object { companion object {
@Provides @Provides
@Singleton @Singleton
fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? { fun providesVoiceBroadcastRecorder(
context: Context,
sessionHolder: ActiveSessionHolder,
getMostRecentVoiceBroadcastStateEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
): VoiceBroadcastRecorder? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
VoiceBroadcastRecorderQ(context) VoiceBroadcastRecorderQ(
context = context,
sessionHolder = sessionHolder,
getVoiceBroadcastEventUseCase = getMostRecentVoiceBroadcastStateEventUseCase
)
} else { } else {
null null
} }

View file

@ -18,6 +18,7 @@ package im.vector.app.features.voicebroadcast.recording
import androidx.annotation.IntRange import androidx.annotation.IntRange
import im.vector.app.features.voice.VoiceRecorder import im.vector.app.features.voice.VoiceRecorder
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import java.io.File import java.io.File
interface VoiceBroadcastRecorder : VoiceRecorder { interface VoiceBroadcastRecorder : VoiceRecorder {
@ -31,7 +32,7 @@ interface VoiceBroadcastRecorder : VoiceRecorder {
/** Current remaining time of recording, in seconds, if any. */ /** Current remaining time of recording, in seconds, if any. */
val currentRemainingTime: Long? val currentRemainingTime: Long?
fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int)
fun addListener(listener: Listener) fun addListener(listener: Listener)
fun removeListener(listener: Listener) fun removeListener(listener: Listener)

View file

@ -20,8 +20,17 @@ import android.content.Context
import android.media.MediaRecorder import android.media.MediaRecorder
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.voice.AbstractVoiceRecorderQ import im.vector.app.features.voice.AbstractVoiceRecorderQ
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
import im.vector.lib.core.utils.timer.CountUpTimer import im.vector.lib.core.utils.timer.CountUpTimer
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@ -30,10 +39,17 @@ import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
class VoiceBroadcastRecorderQ( class VoiceBroadcastRecorderQ(
context: Context, context: Context,
private val sessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase
) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder { ) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder {
private val session get() = sessionHolder.getActiveSession()
private val sessionScope get() = session.coroutineScope
private var voiceBroadcastStateObserver: Job? = null
private var maxFileSize = 0L // zero or negative for no limit private var maxFileSize = 0L // zero or negative for no limit
private var currentRoomId: String? = null private var currentVoiceBroadcast: VoiceBroadcast? = null
private var currentMaxLength: Int = 0 private var currentMaxLength: Int = 0
override var currentSequence = 0 override var currentSequence = 0
@ -68,14 +84,16 @@ class VoiceBroadcastRecorderQ(
} }
} }
override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) { override fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) {
currentRoomId = roomId // Stop recording previous voice broadcast if any
if (recordingState != VoiceBroadcastRecorder.State.Idle) stopRecord()
currentVoiceBroadcast = voiceBroadcast
maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong() maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
currentMaxLength = maxLength currentMaxLength = maxLength
currentSequence = 1 currentSequence = 1
startRecord(roomId)
recordingState = VoiceBroadcastRecorder.State.Recording observeVoiceBroadcastStateEvent(voiceBroadcast)
recordingTicker.start()
} }
override fun pauseRecord() { override fun pauseRecord() {
@ -88,7 +106,7 @@ class VoiceBroadcastRecorderQ(
override fun resumeRecord() { override fun resumeRecord() {
currentSequence++ currentSequence++
currentRoomId?.let { startRecord(it) } currentVoiceBroadcast?.let { startRecord(it.roomId) }
recordingState = VoiceBroadcastRecorder.State.Recording recordingState = VoiceBroadcastRecorder.State.Recording
recordingTicker.resume() recordingTicker.resume()
} }
@ -104,11 +122,15 @@ class VoiceBroadcastRecorderQ(
// Remove listeners // Remove listeners
listeners.clear() listeners.clear()
// Do not observe anymore voice broadcast changes
voiceBroadcastStateObserver?.cancel()
voiceBroadcastStateObserver = null
// Reset data // Reset data
currentSequence = 0 currentSequence = 0
currentMaxLength = 0 currentMaxLength = 0
currentRemainingTime = null currentRemainingTime = null
currentRoomId = null currentVoiceBroadcast = null
} }
override fun release() { override fun release() {
@ -126,6 +148,26 @@ class VoiceBroadcastRecorderQ(
listeners.remove(listener) listeners.remove(listener)
} }
private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) {
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
.onEach { onVoiceBroadcastStateEventUpdated(voiceBroadcast, it.getOrNull()) }
.launchIn(sessionScope)
}
private fun onVoiceBroadcastStateEventUpdated(voiceBroadcast: VoiceBroadcast, event: VoiceBroadcastEvent?) {
when (event?.content?.voiceBroadcastState) {
VoiceBroadcastState.STARTED -> {
startRecord(voiceBroadcast.roomId)
recordingState = VoiceBroadcastRecorder.State.Recording
recordingTicker.start()
}
VoiceBroadcastState.PAUSED -> pauseRecord()
VoiceBroadcastState.RESUMED -> resumeRecord()
VoiceBroadcastState.STOPPED,
null -> stopRecord()
}
}
private fun onMaxFileSizeApproaching(roomId: String) { private fun onMaxFileSizeApproaching(roomId: String) {
setNextOutputFile(roomId) setNextOutputFile(roomId)
} }

View file

@ -62,11 +62,5 @@ class PauseVoiceBroadcastUseCase @Inject constructor(
lastChunkSequence = voiceBroadcastRecorder?.currentSequence, lastChunkSequence = voiceBroadcastRecorder?.currentSequence,
).toContent(), ).toContent(),
) )
pauseRecording()
}
private fun pauseRecording() {
voiceBroadcastRecorder?.pauseRecord()
} }
} }

View file

@ -20,7 +20,6 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
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.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent 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.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
@ -31,8 +30,7 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class ResumeVoiceBroadcastUseCase @Inject constructor( class ResumeVoiceBroadcastUseCase @Inject constructor(
private val session: Session, private val session: Session
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
) { ) {
suspend fun execute(roomId: String): Result<Unit> = runCatching { suspend fun execute(roomId: String): Result<Unit> = runCatching {
@ -66,11 +64,5 @@ class ResumeVoiceBroadcastUseCase @Inject constructor(
voiceBroadcastStateStr = VoiceBroadcastState.RESUMED.value, voiceBroadcastStateStr = VoiceBroadcastState.RESUMED.value,
).toContent(), ).toContent(),
) )
resumeRecording()
}
private fun resumeRecording() {
voiceBroadcastRecorder?.resumeRecord()
} }
} }

View file

@ -24,11 +24,13 @@ import im.vector.app.features.session.coroutineScope
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
import im.vector.lib.multipicker.utils.toMultiPickerAudioType import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
@ -43,6 +45,8 @@ import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
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.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -63,6 +67,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
assertCanStartVoiceBroadcast(room) assertCanStartVoiceBroadcast(room)
startVoiceBroadcast(room) startVoiceBroadcast(room)
return Result.success(Unit)
} }
private suspend fun startVoiceBroadcast(room: Room) { private suspend fun startVoiceBroadcast(room: Room) {
@ -79,13 +84,15 @@ class StartVoiceBroadcastUseCase @Inject constructor(
).toContent() ).toContent()
) )
startRecording(room, eventId, chunkLength, maxLength) val voiceBroadcast = VoiceBroadcast(roomId = room.roomId, voiceBroadcastId = eventId)
room.flow().liveTimelineEvent(eventId).unwrap().first() // wait for the event come back from the sync
startRecording(room, voiceBroadcast, chunkLength, maxLength)
} }
private fun startRecording(room: Room, eventId: String, chunkLength: Int, maxLength: Int) { private fun startRecording(room: Room, voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) {
voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener { voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener {
override fun onVoiceMessageCreated(file: File, sequence: Int) { override fun onVoiceMessageCreated(file: File, sequence: Int) {
sendVoiceFile(room, file, eventId, sequence) sendVoiceFile(room, file, voiceBroadcast, sequence)
} }
override fun onRemainingTimeUpdated(remainingTime: Long?) { override fun onRemainingTimeUpdated(remainingTime: Long?) {
@ -94,10 +101,10 @@ class StartVoiceBroadcastUseCase @Inject constructor(
} }
} }
}) })
voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength, maxLength) voiceBroadcastRecorder?.startRecordVoiceBroadcast(voiceBroadcast, chunkLength, maxLength)
} }
private fun sendVoiceFile(room: Room, voiceMessageFile: File, referenceEventId: String, sequence: Int) { private fun sendVoiceFile(room: Room, voiceMessageFile: File, voiceBroadcast: VoiceBroadcast, sequence: Int) {
val outputFileUri = FileProvider.getUriForFile( val outputFileUri = FileProvider.getUriForFile(
context, context,
buildMeta.applicationId + ".fileProvider", buildMeta.applicationId + ".fileProvider",
@ -109,7 +116,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
attachment = audioType.toContentAttachmentData(isVoiceMessage = true), attachment = audioType.toContentAttachmentData(isVoiceMessage = true),
compressBeforeSending = false, compressBeforeSending = false,
roomIds = emptySet(), roomIds = emptySet(),
relatesTo = RelationDefaultContent(RelationType.REFERENCE, referenceEventId), relatesTo = RelationDefaultContent(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId),
additionalContent = mapOf( additionalContent = mapOf(
VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY to VoiceBroadcastChunk(sequence = sequence).toContent() VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY to VoiceBroadcastChunk(sequence = sequence).toContent()
) )

View file

@ -19,7 +19,6 @@ package im.vector.app.features.voicebroadcast.usecase
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
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.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.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase
import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService import im.vector.app.test.fakes.FakeRoomService
@ -27,7 +26,6 @@ import im.vector.app.test.fakes.FakeSession
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
@ -47,8 +45,7 @@ class ResumeVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom() private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true) private val resumeVoiceBroadcastUseCase = ResumeVoiceBroadcastUseCase(fakeSession)
private val resumeVoiceBroadcastUseCase = ResumeVoiceBroadcastUseCase(fakeSession, fakeVoiceBroadcastRecorder)
@Test @Test
fun `given a room id with a potential existing voice broadcast state when calling execute then the voice broadcast is resumed or not`() = runTest { fun `given a room id with a potential existing voice broadcast state when calling execute then the voice broadcast is resumed or not`() = runTest {