Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,7 @@ public class MessageComposerViewModel(

public fun seekRecordingTo(progress: Float): Unit = messageComposerController.seekRecordingTo(progress)

public fun sendRecording() {
completeRecording()
sendMessage(buildNewMessage(input.value, selectedAttachments.value))
}
public fun sendRecording(): Unit = messageComposerController.sendRecording()

/**
* Disposes the inner [MessageComposerController].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@
import io.getstream.chat.android.client.audio.audioHash
import io.getstream.chat.android.client.extensions.EXTRA_WAVEFORM_DATA
import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider
import io.getstream.chat.android.models.Attachment
import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState
import io.getstream.chat.android.ui.common.state.messages.composer.copy
import io.getstream.log.StreamLog
import io.getstream.log.TaggedLogger
import io.getstream.result.Error
import io.getstream.result.Result
import io.getstream.sdk.chat.audio.recording.StreamMediaRecorder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Date
import kotlin.math.abs
Expand All @@ -41,15 +45,13 @@
/**
* Controller responsible for recording audio messages.
*
* @param channelId The ID of the channel we're chatting in.
* @param audioPlayer The audio player used to play audio messages.
* @param mediaRecorder The media recorder used to record audio messages.
* @param fileToUri Coverts [File] into Uri like string.
* @param scope Coverts [File] into Uri like string.
* @param scope MessageComposerController's scope.
*/
internal class AudioRecordingController(
private val channelId: String,
private val audioPlayer: AudioPlayer,
private val mediaRecorder: StreamMediaRecorder,
private val fileToUri: (File) -> String,
Expand Down Expand Up @@ -158,19 +160,22 @@
}
}

public fun startRecording(offset: Pair<Float, Float>? = null) {
suspend fun startRecording(offset: Pair<Float, Float>? = null) {
val state = this.recordingState.value
if (state !is RecordingState.Idle) {
logger.w { "[startRecording] rejected (state is not Idle): $state" }
return
}
logger.i { "[startRecording] state: $state" }
val recordingName = "audio_recording_${Date()}.aac"
mediaRecorder.startAudioRecording(recordingName, realPollingInterval.toLong())
// Call on Dispatchers.IO because it accesses file system
withContext(DispatcherProvider.IO) {
mediaRecorder.startAudioRecording(recordingName, realPollingInterval.toLong())
}
setState(RecordingState.Hold(offset = offset ?: RecordingState.Hold.ZeroOffset))
}

public fun holdRecording(offset: Pair<Float, Float>? = null) {
fun holdRecording(offset: Pair<Float, Float>? = null) {
val state = this.recordingState.value
if (state !is RecordingState.Hold) {
logger.w { "[holdRecording] rejected (state is not Hold): $state" }
Expand All @@ -184,7 +189,7 @@
setState(state.copy(offset = offset))
}

public fun lockRecording() {
fun lockRecording() {
val state = this.recordingState.value
if (state !is RecordingState.Hold) {
logger.w { "[lockRecording] rejected (state is not Hold): $state" }
Expand All @@ -194,10 +199,10 @@
setState(RecordingState.Locked(state.durationInMs, state.waveform))
}

public fun cancelRecording() {
fun cancelRecording() {
val state = this.recordingState.value
if (state is RecordingState.Idle) {
logger.w { "[cancelRecording] rejected (state is not Idle)" }
logger.w { "[cancelRecording] rejected (state is Idle)" }
return
}
logger.i { "[cancelRecording] state: $state" }
Expand All @@ -209,7 +214,7 @@
setState(RecordingState.Idle)
}

public fun toggleRecordingPlayback() {
fun toggleRecordingPlayback() {
val state = this.recordingState.value
if (state !is RecordingState.Overview) {
logger.v { "[toggleRecordingPlayback] rejected (state is not Locked): $state" }
Expand Down Expand Up @@ -278,14 +283,17 @@
}
}

public fun stopRecording() {
suspend fun stopRecording() {
val state = this.recordingState.value
if (state !is RecordingState.Locked) {
logger.w { "[stopRecording] rejected (state is not Locked): $state" }
return
}
logger.i { "[stopRecording] no args: $state" }
val result = mediaRecorder.stopRecording()
// Call on Dispatchers.IO because it accesses file system
val result = withContext(DispatcherProvider.IO) {
mediaRecorder.stopRecording()
}
if (result.isFailure) {
logger.e { "[stopRecording] failed: ${result.errorOrNull()}" }
clearData()
Expand All @@ -300,7 +308,7 @@
setState(RecordingState.Overview(recorded.durationInMs, normalized, recorded.attachment))
}

public fun seekRecordingTo(progress: Float) {
fun seekRecordingTo(progress: Float) {
val state = this.recordingState.value
if (state !is RecordingState.Overview) {
logger.w { "[seekRecordingTo] rejected (state is not Overview)" }
Expand All @@ -317,7 +325,7 @@
setState(state.copy(playingProgress = progress))
}

public fun pauseRecording() {
fun pauseRecording() {
val state = this.recordingState.value
if (state !is RecordingState.Overview) {
logger.w { "[pauseRecording] rejected (state is not Overview)" }
Expand All @@ -328,7 +336,52 @@
setState(state.copy(isPlaying = false))
}

public fun completeRecording() {
suspend fun completeRecordingSync(): Result<Attachment> {
val state = this.recordingState.value
logger.w { "[completeRecordingSync] state: $state" }
if (state is RecordingState.Idle) {
logger.w { "[completeRecordingSync] rejected (state is Idle)" }
return Result.Failure(Error.GenericError("Recording is in Idle state"))
}
if (state is RecordingState.Overview) {
logger.d { "[completeRecordingSync] completing from Overview state" }
clearData()
val attachment = state.attachment.copy(
extraData = state.attachment.extraData + mapOf(
EXTRA_WAVEFORM_DATA to state.waveform,
),
)
setState(RecordingState.Idle)
return Result.Success(attachment)
}
// Call on Dispatchers.IO because it accesses file system
val result = withContext(DispatcherProvider.IO) {
mediaRecorder.stopRecording()
}
when (result) {

Check warning on line 361 in stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move "return" statements from all branches before "when" statement.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-android&issues=AZr96nkrCNjN-lnvhjIM&open=AZr96nkrCNjN-lnvhjIM&pullRequest=6036
is Result.Failure -> {
logger.e { "[completeRecordingSync] failed: ${result.value}" }
clearData()
setState(RecordingState.Idle)
return result
}

is Result.Success -> {
logger.d { "[completeRecordingSync] complete from state: $state" }
val adjusted = samples.downsampleMax(samplesTarget)
val normalized = adjusted.normalize()
clearData()
val attachment = result.value.attachment
val attachmentWithWaveform = attachment.copy(
extraData = attachment.extraData + mapOf(EXTRA_WAVEFORM_DATA to normalized),
)
setState(RecordingState.Idle)
return Result.Success(attachmentWithWaveform)
}
}
}

suspend fun completeRecording() {
val state = this.recordingState.value
logger.w { "[completeRecording] state: $state" }
if (state is RecordingState.Idle) {
Expand All @@ -339,19 +392,22 @@
logger.d { "[completeRecording] completing from Overview state" }
audioPlayer.resetAudio(state.playingId)
clearData()
setState(
RecordingState.Complete(
state.attachment.copy(
extraData = state.attachment.extraData + mapOf(
EXTRA_WAVEFORM_DATA to state.waveform,
),
),
val complete = RecordingState.Complete(
state.attachment.copy(
extraData = state.attachment.extraData + mapOf(EXTRA_WAVEFORM_DATA to state.waveform),
),
)
setState(RecordingState.Idle)
// Run on Dispatchers.Main to ensure both state updates are published in order (complete and idle)
withContext(DispatcherProvider.Main) {
setState(complete)
setState(RecordingState.Idle)
}
return
}
val result = mediaRecorder.stopRecording()
// Run on Dispatchers.IO because it accesses file system
val result = withContext(DispatcherProvider.IO) {
mediaRecorder.stopRecording()
}
if (result.isFailure) {
logger.e { "[completeRecording] failed: ${result.errorOrNull()}" }
clearData()
Expand All @@ -364,18 +420,19 @@
val recorded = result.getOrThrow().let {
it.copy(
attachment = it.attachment.copy(
extraData = it.attachment.extraData + mapOf(
EXTRA_WAVEFORM_DATA to normalized,
),
extraData = it.attachment.extraData + mapOf(EXTRA_WAVEFORM_DATA to normalized),
),
)
}
logger.d { "[completeRecording] complete from state: $state" }
setState(RecordingState.Complete(recorded.attachment))
setState(RecordingState.Idle)
// Run on Dispatchers.Main to ensure both state updates are published in order (complete and idle)
withContext(DispatcherProvider.Main) {
setState(RecordingState.Complete(recorded.attachment))
setState(RecordingState.Idle)
}
}

public fun onCleared() {
fun onCleared() {
logger.i { "[onCleared] no args" }
mediaRecorder.release()
val state = this.recordingState.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ public class MessageComposerController(
private val scope = CoroutineScope(DispatcherProvider.Immediate)

private val audioRecordingController = AudioRecordingController(
channelCid,
chatClient.audioPlayer,
mediaRecorder,
fileToUri,
Expand Down Expand Up @@ -886,7 +885,9 @@ public class MessageComposerController(
* from [RecordingState.Idle] to [RecordingState.Hold].
*/
public fun startRecording(offset: Pair<Float, Float>? = null) {
audioRecordingController.startRecording(offset)
scope.launch {
audioRecordingController.startRecording(offset)
}
}

/**
Expand Down Expand Up @@ -914,13 +915,21 @@ public class MessageComposerController(
/**
* Stops audio recording and moves [MessageComposerState.recording] state to [RecordingState.Overview].
*/
public fun stopRecording(): Unit = audioRecordingController.stopRecording()
public fun stopRecording() {
scope.launch {
audioRecordingController.stopRecording()
}
}

/**
* Completes audio recording and moves [MessageComposerState.recording] state to [RecordingState.Complete].
* Also, it wil update [MessageComposerState.attachments] list.
*/
public fun completeRecording(): Unit = audioRecordingController.completeRecording()
public fun completeRecording() {
scope.launch {
audioRecordingController.completeRecording()
}
}

/**
* Pauses audio recording and sets [RecordingState.Overview.isPlaying] to false.
Expand All @@ -934,6 +943,18 @@ public class MessageComposerController(
*/
public fun seekRecordingTo(progress: Float): Unit = audioRecordingController.seekRecordingTo(progress)

/**
* Completes the active audio recording and sends the recorded audio as an attachment.
*/
public fun sendRecording() {
scope.launch {
audioRecordingController.completeRecordingSync().onSuccess { recording ->
val attachments = selectedAttachments.value + recording
sendMessage(buildNewMessage(messageInput.value.text, attachments), callback = {})
}
}
}

/**
* Shows the mention suggestion list popup if necessary.
*/
Expand Down
Loading
Loading