Add voice message recording duration indicator and limit (#1628)
--------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
bdc52332bb
commit
9046ac4c8a
22 changed files with 263 additions and 35 deletions
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.libraries.voicerecorder.api
|
||||
|
||||
import java.io.File
|
||||
import kotlin.time.Duration
|
||||
|
||||
sealed class VoiceRecorderState {
|
||||
/**
|
||||
|
|
@ -27,9 +28,10 @@ sealed class VoiceRecorderState {
|
|||
/**
|
||||
* The recorder is currently recording.
|
||||
*
|
||||
* @property elapsedTime The elapsed time since the recording started.
|
||||
* @property level The current audio level of the recording as a fraction of 1.
|
||||
*/
|
||||
data class Recording(val level: Double) : VoiceRecorderState()
|
||||
data class Recording(val elapsedTime: Duration, val level: Double) : VoiceRecorderState()
|
||||
|
||||
/**
|
||||
* The recorder has finished recording.
|
||||
|
|
|
|||
|
|
@ -37,15 +37,19 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
@SingleIn(RoomScope::class)
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class VoiceRecorderImpl @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val timeSource: TimeSource,
|
||||
private val audioReaderFactory: AudioReader.Factory,
|
||||
private val encoder: Encoder,
|
||||
private val fileManager: VoiceFileManager,
|
||||
|
|
@ -74,16 +78,27 @@ class VoiceRecorderImpl @Inject constructor(
|
|||
val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it }
|
||||
|
||||
recordingJob = voiceCoroutineScope.launch {
|
||||
val startedAt = timeSource.markNow()
|
||||
audioRecorder.record { audio ->
|
||||
yield()
|
||||
|
||||
val elapsedTime = startedAt.elapsedNow()
|
||||
|
||||
if (elapsedTime >= 30.minutes) {
|
||||
Timber.w("Voice message time limit reached")
|
||||
stopRecord(false)
|
||||
return@record
|
||||
}
|
||||
|
||||
when (audio) {
|
||||
is Audio.Data -> {
|
||||
val audioLevel = audioLevelCalculator.calculateAudioLevel(audio.buffer)
|
||||
_state.emit(VoiceRecorderState.Recording(audioLevel))
|
||||
_state.emit(VoiceRecorderState.Recording(elapsedTime, audioLevel))
|
||||
encoder.encode(audio.buffer, audio.readSize)
|
||||
}
|
||||
is Audio.Error -> {
|
||||
Timber.e("Voice message error: code=${audio.audioRecordErrorCode}")
|
||||
_state.emit(VoiceRecorderState.Recording(0.0))
|
||||
_state.emit(VoiceRecorderState.Recording(elapsedTime, 0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,9 +37,13 @@ import kotlinx.coroutines.test.runTest
|
|||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TestTimeSource
|
||||
|
||||
class VoiceRecorderImplTest {
|
||||
private val fakeFileSystem = FakeFileSystem()
|
||||
private val timeSource = TestTimeSource()
|
||||
|
||||
@Test
|
||||
fun `it emits the initial state`() = runTest {
|
||||
|
|
@ -56,9 +60,27 @@ class VoiceRecorderImplTest {
|
|||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle)
|
||||
|
||||
voiceRecorder.startRecord()
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0))
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.0))
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0))
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0))
|
||||
timeSource += 1.seconds
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds,0.0))
|
||||
timeSource += 1.seconds
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when elapsed time reaches 30 minutes, it stops recording`() = runTest {
|
||||
val voiceRecorder = createVoiceRecorder()
|
||||
voiceRecorder.state.test {
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle)
|
||||
|
||||
voiceRecorder.startRecord()
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, 1.0))
|
||||
timeSource += 29.minutes
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0))
|
||||
timeSource += 1.minutes
|
||||
|
||||
assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +116,7 @@ class VoiceRecorderImplTest {
|
|||
val fileConfig = VoiceRecorderModule.provideVoiceFileConfig()
|
||||
return VoiceRecorderImpl(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
timeSource = timeSource,
|
||||
audioReaderFactory = FakeAudioRecorderFactory(
|
||||
audio = AUDIO,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ class FakeAudioReader(
|
|||
while (audios.hasNext()) {
|
||||
if (!isRecording) break
|
||||
onAudio(audios.next())
|
||||
yield()
|
||||
}
|
||||
while (isActive) {
|
||||
// do not return from the coroutine until it is cancelled
|
||||
|
|
|
|||
|
|
@ -21,8 +21,13 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.File
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TestTimeSource
|
||||
|
||||
class FakeVoiceRecorder(
|
||||
private val timeSource: TestTimeSource = TestTimeSource(),
|
||||
private val recordingDuration: Duration = 0.seconds,
|
||||
private val levels: List<Double> = listOf(0.1, 0.2)
|
||||
) : VoiceRecorder {
|
||||
private val _state = MutableStateFlow<VoiceRecorderState>(VoiceRecorderState.Idle)
|
||||
|
|
@ -33,6 +38,7 @@ class FakeVoiceRecorder(
|
|||
private var securityException: SecurityException? = null
|
||||
|
||||
override suspend fun startRecord() {
|
||||
val startedAt = timeSource.markNow()
|
||||
securityException?.let { throw it }
|
||||
|
||||
if (curRecording != null) {
|
||||
|
|
@ -40,8 +46,9 @@ class FakeVoiceRecorder(
|
|||
}
|
||||
curRecording = File("file.ogg")
|
||||
|
||||
timeSource += recordingDuration
|
||||
levels.forEach {
|
||||
_state.emit(VoiceRecorderState.Recording(it))
|
||||
_state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), it))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue