Merge pull request #7588 from vector-im/feature/fre/voice_broadcast_recording_time
Voice Broadcast - Add max length for recording
This commit is contained in:
commit
54fcdcdb6d
|
@ -0,0 +1 @@
|
|||
Voice Broadcast - Add maximum length
|
|
@ -3101,6 +3101,8 @@
|
|||
<string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
|
||||
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
|
||||
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
|
||||
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
|
||||
<string name="voice_broadcast_recording_time_left">%1$s left</string>
|
||||
|
||||
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
||||
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.</string>
|
||||
|
|
|
@ -66,7 +66,7 @@ class AudioMessageHelper @Inject constructor(
|
|||
|
||||
fun startRecording(roomId: String) {
|
||||
stopPlayback()
|
||||
playbackTracker.makeAllPlaybacksIdle()
|
||||
playbackTracker.pauseAllPlaybacks()
|
||||
amplitudeList.clear()
|
||||
|
||||
try {
|
||||
|
|
|
@ -51,15 +51,7 @@ class AudioMessagePlaybackTracker @Inject constructor() {
|
|||
}
|
||||
|
||||
fun pauseAllPlaybacks() {
|
||||
listeners.keys.forEach { key ->
|
||||
pausePlayback(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun makeAllPlaybacksIdle() {
|
||||
listeners.keys.forEach { key ->
|
||||
setState(key, Listener.State.Idle)
|
||||
}
|
||||
listeners.keys.forEach(::pausePlayback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,10 +21,12 @@ import androidx.core.view.isVisible
|
|||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.utils.TextUtils
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||
import org.threeten.bp.Duration
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastRecordingItem.Holder>() {
|
||||
|
@ -37,11 +39,15 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
|
||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||
if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) {
|
||||
if (recorder != null && recorder?.recordingState != VoiceBroadcastRecorder.State.Idle) {
|
||||
recorderListener = object : VoiceBroadcastRecorder.Listener {
|
||||
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
||||
renderRecordingState(holder, state)
|
||||
}
|
||||
|
||||
override fun onRemainingTimeUpdated(remainingTime: Long?) {
|
||||
renderRemainingTime(holder, remainingTime)
|
||||
}
|
||||
}.also { recorder?.addListener(it) }
|
||||
} else {
|
||||
renderVoiceBroadcastState(holder)
|
||||
|
@ -58,9 +64,19 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem
|
|||
}
|
||||
|
||||
override fun renderMetadata(holder: Holder) {
|
||||
with(holder) {
|
||||
listenersCountMetadata.isVisible = false
|
||||
remainingTimeMetadata.isVisible = false
|
||||
holder.listenersCountMetadata.isVisible = false
|
||||
}
|
||||
|
||||
private fun renderRemainingTime(holder: Holder, remainingTime: Long?) {
|
||||
if (remainingTime != null) {
|
||||
val formattedDuration = TextUtils.formatDurationWithUnits(
|
||||
holder.view.context,
|
||||
Duration.ofSeconds(remainingTime.coerceAtLeast(0L))
|
||||
)
|
||||
holder.remainingTimeMetadata.value = holder.view.resources.getString(R.string.voice_broadcast_recording_time_left, formattedDuration)
|
||||
holder.remainingTimeMetadata.isVisible = true
|
||||
} else {
|
||||
holder.remainingTimeMetadata.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,4 +28,7 @@ object VoiceBroadcastConstants {
|
|||
|
||||
/** Default voice broadcast chunk duration, in seconds. */
|
||||
const val DEFAULT_CHUNK_LENGTH_IN_SECONDS = 120
|
||||
|
||||
/** Maximum length of the voice broadcast in seconds. */
|
||||
const val MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS = 14_400 // 4 hours
|
||||
}
|
||||
|
|
|
@ -22,16 +22,23 @@ import java.io.File
|
|||
|
||||
interface VoiceBroadcastRecorder : VoiceRecorder {
|
||||
|
||||
/** The current chunk number. */
|
||||
val currentSequence: Int
|
||||
val state: State
|
||||
|
||||
fun startRecord(roomId: String, chunkLength: Int)
|
||||
/** Current state of the recorder. */
|
||||
val recordingState: State
|
||||
|
||||
/** Current remaining time of recording, in seconds, if any. */
|
||||
val currentRemainingTime: Long?
|
||||
|
||||
fun startRecord(roomId: String, chunkLength: Int, maxLength: Int)
|
||||
fun addListener(listener: Listener)
|
||||
fun removeListener(listener: Listener)
|
||||
|
||||
interface Listener {
|
||||
fun onVoiceMessageCreated(file: File, @IntRange(from = 1) sequence: Int) = Unit
|
||||
fun onStateUpdated(state: State) = Unit
|
||||
fun onRemainingTimeUpdated(remainingTime: Long?) = Unit
|
||||
}
|
||||
|
||||
enum class State {
|
||||
|
|
|
@ -21,9 +21,11 @@ import android.media.MediaRecorder
|
|||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import im.vector.app.features.voice.AbstractVoiceRecorderQ
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
class VoiceBroadcastRecorderQ(
|
||||
|
@ -32,13 +34,21 @@ class VoiceBroadcastRecorderQ(
|
|||
|
||||
private var maxFileSize = 0L // zero or negative for no limit
|
||||
private var currentRoomId: String? = null
|
||||
private var currentMaxLength: Int = 0
|
||||
|
||||
override var currentSequence = 0
|
||||
override var state = VoiceBroadcastRecorder.State.Idle
|
||||
override var recordingState = VoiceBroadcastRecorder.State.Idle
|
||||
set(value) {
|
||||
field = value
|
||||
listeners.forEach { it.onStateUpdated(value) }
|
||||
}
|
||||
override var currentRemainingTime: Long? = null
|
||||
set(value) {
|
||||
field = value
|
||||
listeners.forEach { it.onRemainingTimeUpdated(value) }
|
||||
}
|
||||
|
||||
private val recordingTicker = RecordingTicker()
|
||||
private val listeners = CopyOnWriteArrayList<VoiceBroadcastRecorder.Listener>()
|
||||
|
||||
override val outputFormat = MediaRecorder.OutputFormat.MPEG_4
|
||||
|
@ -58,33 +68,47 @@ class VoiceBroadcastRecorderQ(
|
|||
}
|
||||
}
|
||||
|
||||
override fun startRecord(roomId: String, chunkLength: Int) {
|
||||
override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) {
|
||||
currentRoomId = roomId
|
||||
maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong()
|
||||
currentMaxLength = maxLength
|
||||
currentSequence = 1
|
||||
startRecord(roomId)
|
||||
state = VoiceBroadcastRecorder.State.Recording
|
||||
recordingState = VoiceBroadcastRecorder.State.Recording
|
||||
recordingTicker.start()
|
||||
}
|
||||
|
||||
override fun pauseRecord() {
|
||||
tryOrNull { mediaRecorder?.stop() }
|
||||
mediaRecorder?.reset()
|
||||
recordingState = VoiceBroadcastRecorder.State.Paused
|
||||
recordingTicker.pause()
|
||||
notifyOutputFileCreated()
|
||||
state = VoiceBroadcastRecorder.State.Paused
|
||||
}
|
||||
|
||||
override fun resumeRecord() {
|
||||
currentSequence++
|
||||
currentRoomId?.let { startRecord(it) }
|
||||
state = VoiceBroadcastRecorder.State.Recording
|
||||
recordingState = VoiceBroadcastRecorder.State.Recording
|
||||
recordingTicker.resume()
|
||||
}
|
||||
|
||||
override fun stopRecord() {
|
||||
super.stopRecord()
|
||||
|
||||
// Stop recording
|
||||
recordingState = VoiceBroadcastRecorder.State.Idle
|
||||
recordingTicker.stop()
|
||||
notifyOutputFileCreated()
|
||||
|
||||
// Remove listeners
|
||||
listeners.clear()
|
||||
|
||||
// Reset data
|
||||
currentSequence = 0
|
||||
state = VoiceBroadcastRecorder.State.Idle
|
||||
currentMaxLength = 0
|
||||
currentRemainingTime = null
|
||||
currentRoomId = null
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
|
@ -94,7 +118,8 @@ class VoiceBroadcastRecorderQ(
|
|||
|
||||
override fun addListener(listener: VoiceBroadcastRecorder.Listener) {
|
||||
listeners.add(listener)
|
||||
listener.onStateUpdated(state)
|
||||
listener.onStateUpdated(recordingState)
|
||||
listener.onRemainingTimeUpdated(currentRemainingTime)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: VoiceBroadcastRecorder.Listener) {
|
||||
|
@ -117,4 +142,53 @@ class VoiceBroadcastRecorderQ(
|
|||
nextOutputFile = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun onElapsedTimeUpdated(elapsedTimeMillis: Long) {
|
||||
currentRemainingTime = if (currentMaxLength > 0 && recordingState != VoiceBroadcastRecorder.State.Idle) {
|
||||
val currentMaxLengthMillis = TimeUnit.SECONDS.toMillis(currentMaxLength.toLong())
|
||||
val remainingTimeMillis = currentMaxLengthMillis - elapsedTimeMillis
|
||||
TimeUnit.MILLISECONDS.toSeconds(remainingTimeMillis)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RecordingTicker(
|
||||
private var recordingTicker: CountUpTimer? = null,
|
||||
) {
|
||||
fun start() {
|
||||
recordingTicker?.stop()
|
||||
recordingTicker = CountUpTimer().apply {
|
||||
tickListener = CountUpTimer.TickListener { onTick(elapsedTime()) }
|
||||
resume()
|
||||
onTick(elapsedTime())
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
recordingTicker?.apply {
|
||||
pause()
|
||||
onTick(elapsedTime())
|
||||
}
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
recordingTicker?.apply {
|
||||
resume()
|
||||
onTick(elapsedTime())
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
recordingTicker?.apply {
|
||||
stop()
|
||||
onTick(elapsedTime())
|
||||
recordingTicker = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTick(elapsedTimeMillis: Long) {
|
||||
onElapsedTimeUpdated(elapsedTimeMillis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import androidx.core.content.FileProvider
|
||||
import im.vector.app.core.resources.BuildMeta
|
||||
import im.vector.app.features.attachments.toContentAttachmentData
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
|
||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
|
||||
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
|
||||
|
@ -28,6 +29,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
|||
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
|
||||
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
|
||||
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -51,6 +53,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
private val context: Context,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
|
||||
private val stopVoiceBroadcastUseCase: StopVoiceBroadcastUseCase,
|
||||
) {
|
||||
|
||||
suspend fun execute(roomId: String): Result<Unit> = runCatching {
|
||||
|
@ -64,7 +67,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
|
||||
private suspend fun startVoiceBroadcast(room: Room) {
|
||||
Timber.d("## StartVoiceBroadcastUseCase: Send new voice broadcast info state event")
|
||||
val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the length from the room settings
|
||||
val chunkLength = VoiceBroadcastConstants.DEFAULT_CHUNK_LENGTH_IN_SECONDS // Todo Get the chunk length from the room settings
|
||||
val maxLength = VoiceBroadcastConstants.MAX_VOICE_BROADCAST_LENGTH_IN_SECONDS // Todo Get the max length from the room settings
|
||||
val eventId = room.stateService().sendStateEvent(
|
||||
eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
|
||||
stateKey = session.myUserId,
|
||||
|
@ -75,16 +79,22 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
).toContent()
|
||||
)
|
||||
|
||||
startRecording(room, eventId, chunkLength)
|
||||
startRecording(room, eventId, chunkLength, maxLength)
|
||||
}
|
||||
|
||||
private fun startRecording(room: Room, eventId: String, chunkLength: Int) {
|
||||
private fun startRecording(room: Room, eventId: String, chunkLength: Int, maxLength: Int) {
|
||||
voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener {
|
||||
override fun onVoiceMessageCreated(file: File, sequence: Int) {
|
||||
sendVoiceFile(room, file, eventId, sequence)
|
||||
}
|
||||
|
||||
override fun onRemainingTimeUpdated(remainingTime: Long?) {
|
||||
if (remainingTime != null && remainingTime <= 0) {
|
||||
session.coroutineScope.launch { stopVoiceBroadcastUseCase.execute(room.roomId) }
|
||||
}
|
||||
}
|
||||
})
|
||||
voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength)
|
||||
voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength, maxLength)
|
||||
}
|
||||
|
||||
private fun sendVoiceFile(room: Room, voiceMessageFile: File, referenceEventId: String, sequence: Int) {
|
||||
|
@ -127,7 +137,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
|
|||
@VisibleForTesting
|
||||
fun assertNoOngoingVoiceBroadcast(room: Room) {
|
||||
when {
|
||||
voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> {
|
||||
voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Recording ||
|
||||
voiceBroadcastRecorder?.recordingState == VoiceBroadcastRecorder.State.Paused -> {
|
||||
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast")
|
||||
throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
context = FakeContext().instance,
|
||||
buildMeta = mockk(),
|
||||
getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
|
||||
stopVoiceBroadcastUseCase = mockk()
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -67,7 +68,7 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
fun setup() {
|
||||
every { fakeRoom.roomId } returns A_ROOM_ID
|
||||
justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) }
|
||||
every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle
|
||||
every { fakeVoiceBroadcastRecorder.recordingState } returns VoiceBroadcastRecorder.State.Idle
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in New Issue