@@ -22,14 +22,18 @@ import io.getstream.chat.android.client.audio.ProgressData
2222import io.getstream.chat.android.client.audio.audioHash
2323import io.getstream.chat.android.client.extensions.EXTRA_WAVEFORM_DATA
2424import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider
25+ import io.getstream.chat.android.models.Attachment
2526import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState
2627import io.getstream.chat.android.ui.common.state.messages.composer.copy
2728import io.getstream.log.StreamLog
2829import io.getstream.log.TaggedLogger
30+ import io.getstream.result.Error
31+ import io.getstream.result.Result
2932import io.getstream.sdk.chat.audio.recording.StreamMediaRecorder
3033import kotlinx.coroutines.CoroutineScope
3134import kotlinx.coroutines.flow.MutableStateFlow
3235import kotlinx.coroutines.launch
36+ import kotlinx.coroutines.withContext
3337import java.io.File
3438import java.util.Date
3539import kotlin.math.abs
@@ -41,15 +45,13 @@ import kotlin.math.sqrt
4145/* *
4246 * Controller responsible for recording audio messages.
4347 *
44- * @param channelId The ID of the channel we're chatting in.
4548 * @param audioPlayer The audio player used to play audio messages.
4649 * @param mediaRecorder The media recorder used to record audio messages.
4750 * @param fileToUri Coverts [File] into Uri like string.
4851 * @param scope Coverts [File] into Uri like string.
4952 * @param scope MessageComposerController's scope.
5053 */
5154internal class AudioRecordingController (
52- private val channelId : String ,
5355 private val audioPlayer : AudioPlayer ,
5456 private val mediaRecorder : StreamMediaRecorder ,
5557 private val fileToUri : (File ) -> String ,
@@ -158,19 +160,22 @@ internal class AudioRecordingController(
158160 }
159161 }
160162
161- public fun startRecording (offset : Pair <Float , Float >? = null) {
163+ suspend fun startRecording (offset : Pair <Float , Float >? = null) {
162164 val state = this .recordingState.value
163165 if (state !is RecordingState .Idle ) {
164166 logger.w { " [startRecording] rejected (state is not Idle): $state " }
165167 return
166168 }
167169 logger.i { " [startRecording] state: $state " }
168170 val recordingName = " audio_recording_${Date ()} .aac"
169- mediaRecorder.startAudioRecording(recordingName, realPollingInterval.toLong())
171+ // Call on Dispatchers.IO because it accesses file system
172+ withContext(DispatcherProvider .IO ) {
173+ mediaRecorder.startAudioRecording(recordingName, realPollingInterval.toLong())
174+ }
170175 setState(RecordingState .Hold (offset = offset ? : RecordingState .Hold .ZeroOffset ))
171176 }
172177
173- public fun holdRecording (offset : Pair <Float , Float >? = null) {
178+ fun holdRecording (offset : Pair <Float , Float >? = null) {
174179 val state = this .recordingState.value
175180 if (state !is RecordingState .Hold ) {
176181 logger.w { " [holdRecording] rejected (state is not Hold): $state " }
@@ -184,7 +189,7 @@ internal class AudioRecordingController(
184189 setState(state.copy(offset = offset))
185190 }
186191
187- public fun lockRecording () {
192+ fun lockRecording () {
188193 val state = this .recordingState.value
189194 if (state !is RecordingState .Hold ) {
190195 logger.w { " [lockRecording] rejected (state is not Hold): $state " }
@@ -194,10 +199,10 @@ internal class AudioRecordingController(
194199 setState(RecordingState .Locked (state.durationInMs, state.waveform))
195200 }
196201
197- public fun cancelRecording () {
202+ fun cancelRecording () {
198203 val state = this .recordingState.value
199204 if (state is RecordingState .Idle ) {
200- logger.w { " [cancelRecording] rejected (state is not Idle)" }
205+ logger.w { " [cancelRecording] rejected (state is Idle)" }
201206 return
202207 }
203208 logger.i { " [cancelRecording] state: $state " }
@@ -209,7 +214,7 @@ internal class AudioRecordingController(
209214 setState(RecordingState .Idle )
210215 }
211216
212- public fun toggleRecordingPlayback () {
217+ fun toggleRecordingPlayback () {
213218 val state = this .recordingState.value
214219 if (state !is RecordingState .Overview ) {
215220 logger.v { " [toggleRecordingPlayback] rejected (state is not Locked): $state " }
@@ -278,14 +283,17 @@ internal class AudioRecordingController(
278283 }
279284 }
280285
281- public fun stopRecording () {
286+ suspend fun stopRecording () {
282287 val state = this .recordingState.value
283288 if (state !is RecordingState .Locked ) {
284289 logger.w { " [stopRecording] rejected (state is not Locked): $state " }
285290 return
286291 }
287292 logger.i { " [stopRecording] no args: $state " }
288- val result = mediaRecorder.stopRecording()
293+ // Call on Dispatchers.IO because it accesses file system
294+ val result = withContext(DispatcherProvider .IO ) {
295+ mediaRecorder.stopRecording()
296+ }
289297 if (result.isFailure) {
290298 logger.e { " [stopRecording] failed: ${result.errorOrNull()} " }
291299 clearData()
@@ -300,7 +308,7 @@ internal class AudioRecordingController(
300308 setState(RecordingState .Overview (recorded.durationInMs, normalized, recorded.attachment))
301309 }
302310
303- public fun seekRecordingTo (progress : Float ) {
311+ fun seekRecordingTo (progress : Float ) {
304312 val state = this .recordingState.value
305313 if (state !is RecordingState .Overview ) {
306314 logger.w { " [seekRecordingTo] rejected (state is not Overview)" }
@@ -317,7 +325,7 @@ internal class AudioRecordingController(
317325 setState(state.copy(playingProgress = progress))
318326 }
319327
320- public fun pauseRecording () {
328+ fun pauseRecording () {
321329 val state = this .recordingState.value
322330 if (state !is RecordingState .Overview ) {
323331 logger.w { " [pauseRecording] rejected (state is not Overview)" }
@@ -328,7 +336,52 @@ internal class AudioRecordingController(
328336 setState(state.copy(isPlaying = false ))
329337 }
330338
331- public fun completeRecording () {
339+ suspend fun completeRecordingSync (): Result <Attachment > {
340+ val state = this .recordingState.value
341+ logger.w { " [completeRecordingSync] state: $state " }
342+ if (state is RecordingState .Idle ) {
343+ logger.w { " [completeRecordingSync] rejected (state is Idle)" }
344+ return Result .Failure (Error .GenericError (" Recording is in Idle state" ))
345+ }
346+ if (state is RecordingState .Overview ) {
347+ logger.d { " [completeRecordingSync] completing from Overview state" }
348+ clearData()
349+ val attachment = state.attachment.copy(
350+ extraData = state.attachment.extraData + mapOf (
351+ EXTRA_WAVEFORM_DATA to state.waveform,
352+ ),
353+ )
354+ setState(RecordingState .Idle )
355+ return Result .Success (attachment)
356+ }
357+ // Call on Dispatchers.IO because it accesses file system
358+ val result = withContext(DispatcherProvider .IO ) {
359+ mediaRecorder.stopRecording()
360+ }
361+ when (result) {
362+ is Result .Failure -> {
363+ logger.e { " [completeRecordingSync] failed: ${result.value} " }
364+ clearData()
365+ setState(RecordingState .Idle )
366+ return result
367+ }
368+
369+ is Result .Success -> {
370+ logger.d { " [completeRecordingSync] complete from state: $state " }
371+ val adjusted = samples.downsampleMax(samplesTarget)
372+ val normalized = adjusted.normalize()
373+ clearData()
374+ val attachment = result.value.attachment
375+ val attachmentWithWaveform = attachment.copy(
376+ extraData = attachment.extraData + mapOf (EXTRA_WAVEFORM_DATA to normalized),
377+ )
378+ setState(RecordingState .Idle )
379+ return Result .Success (attachmentWithWaveform)
380+ }
381+ }
382+ }
383+
384+ suspend fun completeRecording () {
332385 val state = this .recordingState.value
333386 logger.w { " [completeRecording] state: $state " }
334387 if (state is RecordingState .Idle ) {
@@ -339,19 +392,22 @@ internal class AudioRecordingController(
339392 logger.d { " [completeRecording] completing from Overview state" }
340393 audioPlayer.resetAudio(state.playingId)
341394 clearData()
342- setState(
343- RecordingState .Complete (
344- state.attachment.copy(
345- extraData = state.attachment.extraData + mapOf (
346- EXTRA_WAVEFORM_DATA to state.waveform,
347- ),
348- ),
395+ val complete = RecordingState .Complete (
396+ state.attachment.copy(
397+ extraData = state.attachment.extraData + mapOf (EXTRA_WAVEFORM_DATA to state.waveform),
349398 ),
350399 )
351- setState(RecordingState .Idle )
400+ // Run on Dispatchers.Main to ensure both state updates are published in order (complete and idle)
401+ withContext(DispatcherProvider .Main ) {
402+ setState(complete)
403+ setState(RecordingState .Idle )
404+ }
352405 return
353406 }
354- val result = mediaRecorder.stopRecording()
407+ // Run on Dispatchers.IO because it accesses file system
408+ val result = withContext(DispatcherProvider .IO ) {
409+ mediaRecorder.stopRecording()
410+ }
355411 if (result.isFailure) {
356412 logger.e { " [completeRecording] failed: ${result.errorOrNull()} " }
357413 clearData()
@@ -364,18 +420,19 @@ internal class AudioRecordingController(
364420 val recorded = result.getOrThrow().let {
365421 it.copy(
366422 attachment = it.attachment.copy(
367- extraData = it.attachment.extraData + mapOf (
368- EXTRA_WAVEFORM_DATA to normalized,
369- ),
423+ extraData = it.attachment.extraData + mapOf (EXTRA_WAVEFORM_DATA to normalized),
370424 ),
371425 )
372426 }
373427 logger.d { " [completeRecording] complete from state: $state " }
374- setState(RecordingState .Complete (recorded.attachment))
375- setState(RecordingState .Idle )
428+ // Run on Dispatchers.Main to ensure both state updates are published in order (complete and idle)
429+ withContext(DispatcherProvider .Main ) {
430+ setState(RecordingState .Complete (recorded.attachment))
431+ setState(RecordingState .Idle )
432+ }
376433 }
377434
378- public fun onCleared () {
435+ fun onCleared () {
379436 logger.i { " [onCleared] no args" }
380437 mediaRecorder.release()
381438 val state = this .recordingState.value
0 commit comments