Skip to content
1 change: 1 addition & 0 deletions changelog.d/7629.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Voice Broadcast - Handle redaction of the state events on the listener and recorder sides
13 changes: 11 additions & 2 deletions vector/src/main/java/im/vector/app/core/di/VoiceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.recording.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
Expand All @@ -36,9 +37,17 @@ abstract class VoiceModule {
companion object {
@Provides
@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) {
VoiceBroadcastRecorderQ(context)
VoiceBroadcastRecorderQ(
context = context,
sessionHolder = sessionHolder,
getVoiceBroadcastEventUseCase = getMostRecentVoiceBroadcastStateEventUseCase
)
} else {
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat
import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent
import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
import im.vector.lib.core.utils.timer.CountUpTimer
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
Expand All @@ -48,7 +48,7 @@ import javax.inject.Singleton
class VoiceBroadcastPlayerImpl @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val playbackTracker: AudioMessagePlaybackTracker,
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase
) : VoiceBroadcastPlayer {

Expand All @@ -66,7 +66,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private var nextMediaPlayer: MediaPlayer? = null
private var isPreparingNextPlayer: Boolean = false

private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null
private var mostRecentVoiceBroadcastEvent: VoiceBroadcastEvent? = null

override var currentVoiceBroadcast: VoiceBroadcast? = null
override var isLiveListening: Boolean = false
Expand Down Expand Up @@ -121,7 +121,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
// Clear playlist
playlist.reset()

currentVoiceBroadcastEvent = null
mostRecentVoiceBroadcastEvent = null
currentVoiceBroadcast = null
}

Expand All @@ -145,19 +145,25 @@ class VoiceBroadcastPlayerImpl @Inject constructor(

playingState = State.BUFFERING

observeVoiceBroadcastLiveState(voiceBroadcast)
observeVoiceBroadcastStateEvent(voiceBroadcast)
fetchPlaylistAndStartPlayback(voiceBroadcast)
}

private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) {
private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) {
voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
.onEach {
currentVoiceBroadcastEvent = it.getOrNull()
updateLiveListeningMode()
}
.onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) }
.launchIn(sessionScope)
}

private fun onVoiceBroadcastStateEventUpdated(event: VoiceBroadcastEvent?) {
if (event == null) {
stop()
} else {
mostRecentVoiceBroadcastEvent = event
updateLiveListeningMode()
}
}

private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
.onEach {
Expand Down Expand Up @@ -198,7 +204,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(

val playlistItem = when {
position != null -> playlist.findByPosition(position)
currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
mostRecentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull()
else -> playlist.firstOrNull()
}
val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return }
Expand Down Expand Up @@ -340,7 +346,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun updateLiveListeningMode(seekPosition: Int? = null) {
isLiveListening = when {
// the current voice broadcast is not live (ended)
currentVoiceBroadcastEvent?.isLive?.not().orFalse() -> false
mostRecentVoiceBroadcastEvent?.isLive != true -> false
// the player is stopped or paused
playingState == State.IDLE || playingState == State.PAUSED -> false
seekPosition != null -> {
Expand Down Expand Up @@ -406,13 +412,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
override fun onCompletion(mp: MediaPlayer) {
if (nextMediaPlayer != null) return

val content = currentVoiceBroadcastEvent?.content
val isLive = content?.isLive.orFalse()
if (!isLive && content?.lastChunkSequence == playlist.currentSequence) {
if (isLiveListening || mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) {
playingState = State.BUFFERING
} else {
// We'll not receive new chunks anymore so we can stop the live listening
stop()
} else {
playingState = State.BUFFERING
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ 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.GetVoiceBroadcastEventUseCase
import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase
import im.vector.app.features.voicebroadcast.voiceBroadcastId
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
Expand All @@ -48,7 +48,7 @@ import javax.inject.Inject
*/
class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase,
private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase,
) {

fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<MessageAudioEvent>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package im.vector.app.features.voicebroadcast.recording

import androidx.annotation.IntRange
import im.vector.app.features.voice.VoiceRecorder
import im.vector.app.features.voicebroadcast.model.VoiceBroadcast
import java.io.File

interface VoiceBroadcastRecorder : VoiceRecorder {
Expand All @@ -31,7 +32,7 @@ interface VoiceBroadcastRecorder : VoiceRecorder {
/** Current remaining time of recording, in seconds, if any. */
val currentRemainingTime: Long?

fun startRecord(roomId: String, chunkLength: Int, maxLength: Int)
fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we need to add VoiceBroadcast in the name of the method? I personnally think this is enough with startRecord since first parameter is a VoiceBroadcast.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My problem is that I want to distinguish this method from the startRecord which is inherited from VoiceRecorder to avoid confusion. Ideally, I would like to only expose this one to the outside and change the visibility of the other method to "protected", but afaik I can do it programmatically but this is a bit ugly imo. Wdyt @mnaturel?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I didn't see the method in the VoiceRecorder Interface. Let's keep it like this then.

fun addListener(listener: Listener)
fun removeListener(listener: Listener)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,17 @@ import android.content.Context
import android.media.MediaRecorder
import android.os.Build
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.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 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.session.content.ContentAttachmentData
import java.util.concurrent.CopyOnWriteArrayList
Expand All @@ -30,10 +39,17 @@ import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.Q)
class VoiceBroadcastRecorderQ(
context: Context,
private val sessionHolder: ActiveSessionHolder,
private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase
) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder {

private val session get() = sessionHolder.getActiveSession()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered it may throw an exception when getting the session variable?

private val sessionScope get() = session.coroutineScope

private var voiceBroadcastStateObserver: Job? = null

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

override var currentSequence = 0
Expand Down Expand Up @@ -68,17 +84,20 @@ class VoiceBroadcastRecorderQ(
}
}

override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) {
currentRoomId = roomId
override fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) {
// Stop recording previous voice broadcast if any
if (recordingState != VoiceBroadcastRecorder.State.Idle) stopRecord()

currentVoiceBroadcast = voiceBroadcast
maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
currentMaxLength = maxLength
currentSequence = 1
startRecord(roomId)
recordingState = VoiceBroadcastRecorder.State.Recording
recordingTicker.start()

observeVoiceBroadcastStateEvent(voiceBroadcast)
}

override fun pauseRecord() {
if (recordingState != VoiceBroadcastRecorder.State.Recording) return
tryOrNull { mediaRecorder?.stop() }
mediaRecorder?.reset()
recordingState = VoiceBroadcastRecorder.State.Paused
Expand All @@ -87,8 +106,9 @@ class VoiceBroadcastRecorderQ(
}

override fun resumeRecord() {
if (recordingState != VoiceBroadcastRecorder.State.Paused) return
currentSequence++
currentRoomId?.let { startRecord(it) }
currentVoiceBroadcast?.let { startRecord(it.roomId) }
recordingState = VoiceBroadcastRecorder.State.Recording
recordingTicker.resume()
}
Expand All @@ -104,11 +124,15 @@ class VoiceBroadcastRecorderQ(
// Remove listeners
listeners.clear()

// Do not observe anymore voice broadcast changes
voiceBroadcastStateObserver?.cancel()
voiceBroadcastStateObserver = null

// Reset data
currentSequence = 0
currentMaxLength = 0
currentRemainingTime = null
currentRoomId = null
currentVoiceBroadcast = null
}

override fun release() {
Expand All @@ -126,6 +150,26 @@ class VoiceBroadcastRecorderQ(
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) {
setNextOutputFile(roomId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,20 @@ class PauseVoiceBroadcastUseCase @Inject constructor(

private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?) {
Timber.d("## PauseVoiceBroadcastUseCase: Send new voice broadcast info state event")

// save the last sequence number and immediately pause the recording
val lastSequence = voiceBroadcastRecorder?.currentSequence
pauseRecording()

room.stateService().sendStateEvent(
eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
stateKey = session.myUserId,
body = MessageVoiceBroadcastInfoContent(
relatesTo = reference,
voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value,
lastChunkSequence = voiceBroadcastRecorder?.currentSequence,
lastChunkSequence = lastSequence,
).toContent(),
)

pauseRecording()
}

private fun pauseRecording() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand All @@ -32,7 +31,6 @@ import javax.inject.Inject

class ResumeVoiceBroadcastUseCase @Inject constructor(
private val session: Session,
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
) {

suspend fun execute(roomId: String): Result<Unit> = runCatching {
Expand Down Expand Up @@ -66,11 +64,5 @@ class ResumeVoiceBroadcastUseCase @Inject constructor(
voiceBroadcastStateStr = VoiceBroadcastState.RESUMED.value,
).toContent(),
)

resumeRecording()
}

private fun resumeRecording() {
voiceBroadcastRecorder?.resumeRecord()
}
}
Loading