WIP grabador en compose

This commit is contained in:
erik-everardo 2025-01-27 00:16:49 -06:00
parent 77d1cc13b4
commit ca80dfab2a
4 changed files with 133 additions and 30 deletions

View File

@ -1,6 +1,8 @@
package com.isolaatti.audio.common.components package com.isolaatti.audio.common.components
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -13,6 +15,11 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -32,9 +39,10 @@ fun AudioPlayer(
isLoading: Boolean, isLoading: Boolean,
durationMs: Long, durationMs: Long,
onPlay: () -> Unit = {}, onPlay: () -> Unit = {},
onPause: () -> Unit = {} onPause: () -> Unit = {},
onSeek: (position: Float) -> Unit = {}
) { ) {
val sliderValue = positionMs.coerceAtLeast(1).toFloat() / durationMs.coerceAtLeast(1).toFloat()
@ -51,7 +59,27 @@ fun AudioPlayer(
if(isLoading) { if(isLoading) {
LinearProgressIndicator(modifier = Modifier.padding(horizontal = 4.dp).weight(1f)) LinearProgressIndicator(modifier = Modifier.padding(horizontal = 4.dp).weight(1f))
} else { } else {
Slider(value = sliderValue, onValueChange = {}, modifier = Modifier.padding(horizontal = 4.dp).weight(1f)) var value by remember { mutableFloatStateOf(0f) }
val interactionSource = remember { MutableInteractionSource() }
val isDragged by interactionSource.collectIsDraggedAsState()
LaunchedEffect(positionMs) {
if(!isDragged) {
value = positionMs.toFloat()
}
}
Slider(
value = value,
onValueChange = {
value = it
},
onValueChangeFinished = {
onSeek(value)
},
modifier = Modifier.padding(horizontal = 4.dp).weight(1f),
valueRange = 0f..durationMs.toFloat()
)
} }
Text("${positionMs.milliseconds.clockFormat()}/${durationMs.milliseconds.clockFormat()}") Text("${positionMs.milliseconds.clockFormat()}/${durationMs.milliseconds.clockFormat()}")

View File

@ -56,6 +56,7 @@ fun AudioRecorder(
isLoading: Boolean, isLoading: Boolean,
durationSeconds: Long, durationSeconds: Long,
position: Long, position: Long,
onSeek: (position: Float) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -67,9 +68,9 @@ fun AudioRecorder(
var duration by remember { mutableLongStateOf(0L) } var duration by remember { mutableLongStateOf(0L) }
LaunchedEffect(isPlaying) { LaunchedEffect(isRecording, recordIsPaused) {
coroutineScope { coroutineScope {
while(isPlaying) { while(isRecording && !recordIsPaused) {
duration += 500 duration += 500
delay(500.milliseconds) delay(500.milliseconds)
} }
@ -93,7 +94,7 @@ fun AudioRecorder(
when { when {
audioRecordPermissionState.status.isGranted -> { audioRecordPermissionState.status.isGranted -> {
when { when {
!isRecording -> { !isRecording && !recordIsStopped-> {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@ -112,7 +113,7 @@ fun AudioRecorder(
} }
} }
isRecording -> { isRecording && !recordIsStopped -> {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp)
@ -121,9 +122,10 @@ fun AudioRecorder(
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
IconButton(onClick = { IconButton(onClick = {
if(recordIsPaused) { if(recordIsPaused) {
onPauseRecording()
} else {
onStartRecording(true) onStartRecording(true)
} else {
onPauseRecording()
} }
}) { }) {
@ -141,7 +143,7 @@ fun AudioRecorder(
} }
} }
recordIsStopped -> { else -> {
AudioPlayer( AudioPlayer(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
positionMs = position, positionMs = position,
@ -149,7 +151,8 @@ fun AudioRecorder(
isPlaying = isPlaying, isPlaying = isPlaying,
durationMs = durationSeconds, durationMs = durationSeconds,
onPlay = onPlay, onPlay = onPlay,
onPause = onPause onPause = onPause,
onSeek = onSeek
) )
} }
} }

View File

@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -68,6 +69,12 @@ class CreatePostViewModel @Inject constructor(
var ended: Boolean = false var ended: Boolean = false
// endregion // endregion
// region Recorder state
var isRecording: MutableStateFlow<Boolean> = MutableStateFlow(false)
var recordingIsPaused: MutableStateFlow<Boolean> = MutableStateFlow(false)
var recodingIsStopped: MutableStateFlow<Boolean> = MutableStateFlow(false)
// endregion
/** /**
* postDiscussion() and editDiscussion() will check for audios pending to upload (drafts). It will * postDiscussion() and editDiscussion() will check for audios pending to upload (drafts). It will

View File

@ -67,9 +67,11 @@ import com.isolaatti.posting.posts.components.PostAttachments
import com.isolaatti.posting.posts.domain.PostingSteps import com.isolaatti.posting.posts.domain.PostingSteps
import com.isolaatti.posting.posts.presentation.CreatePostViewModel import com.isolaatti.posting.posts.presentation.CreatePostViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.guava.await import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.Calendar import java.util.Calendar
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -144,6 +146,10 @@ class CreatePostActivity : IsolaattiBaseActivity() {
} }
} }
private lateinit var recordOutputFile: File
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
@ -152,13 +158,27 @@ class CreatePostActivity : IsolaattiBaseActivity() {
val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java)) val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java))
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync() mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(this) else MediaRecorder()
lifecycleScope.launch { lifecycleScope.launch {
mediaController = mediaControllerFuture.await() mediaController = mediaControllerFuture.await()
mediaController.addListener(playerListener) mediaController.addListener(playerListener)
// check audios directory
val directory = File(this@CreatePostActivity.filesDir, "recordings")
withContext(Dispatchers.IO) {
if(!directory.isDirectory) {
if(directory.isFile) {
directory.delete()
}
directory.mkdir()
}
}
recordOutputFile = File(directory, "${System.currentTimeMillis()}.3gp")
setContent { setContent {
val text by viewModel.liveContent.observeAsState("") val text by viewModel.liveContent.observeAsState("")
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -257,6 +277,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
IconButton( IconButton(
onClick = { onClick = {
audioRecorderIsVisible = !audioRecorderIsVisible audioRecorderIsVisible = !audioRecorderIsVisible
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(this@CreatePostActivity) else MediaRecorder()
} }
) { ) {
Icon(painterResource(id = R.drawable.baseline_mic_24), null) Icon(painterResource(id = R.drawable.baseline_mic_24), null)
@ -275,7 +296,9 @@ class CreatePostActivity : IsolaattiBaseActivity() {
val duration by viewModel.duration.collectAsState() val duration by viewModel.duration.collectAsState()
val isLoading by viewModel.isLoading.collectAsState() val isLoading by viewModel.isLoading.collectAsState()
val outputFile = File(this@CreatePostActivity.filesDir, "${System.currentTimeMillis()}.3gp") val isRecording by viewModel.isRecording.collectAsState()
val recordIsPaused by viewModel.recordingIsPaused.collectAsState()
val recordIsStopped by viewModel.recodingIsStopped.collectAsState()
LaunchedEffect(isPlaying) { LaunchedEffect(isPlaying) {
while(isPlaying) { while(isPlaying) {
@ -288,6 +311,29 @@ class CreatePostActivity : IsolaattiBaseActivity() {
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
onDismiss = { onDismiss = {
audioRecorderIsVisible = false audioRecorderIsVisible = false
// stop playback
mediaController.stop()
mediaController.clearMediaItems()
// delete file
lifecycleScope.launch(Dispatchers.IO) {
if(recordOutputFile.exists()) {
recordOutputFile.delete()
}
}
// release media recorder
mediaRecorder.release()
// clear references
viewModel.recodingIsStopped.value = false
viewModel.isRecording.value = false
viewModel.recordingIsPaused.value = false
viewModel.isPlaying.value = false
viewModel.duration.value = 0L
viewModel.position.value = 0L
viewModel.audioToUpload = null
}, },
onPlay = { onPlay = {
if(mediaController.isCommandAvailable(Player.COMMAND_PREPARE)) { if(mediaController.isCommandAvailable(Player.COMMAND_PREPARE)) {
@ -302,28 +348,41 @@ class CreatePostActivity : IsolaattiBaseActivity() {
onPause = { onPause = {
mediaController.pause() mediaController.pause()
}, },
onStartRecording = { onStartRecording = { fromPause ->
mediaRecorder.run { if(fromPause) {
setAudioSource(MediaRecorder.AudioSource.MIC) mediaRecorder.resume()
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) } else {
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) mediaRecorder.run {
if (Build.VERSION.SDK_INT >= 26) { setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFile(outputFile) setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
} else { setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
setOutputFile(outputFile.path) if (Build.VERSION.SDK_INT >= 26) {
setOutputFile(recordOutputFile)
} else {
setOutputFile(recordOutputFile.path)
}
prepare()
start()
} }
prepare()
start()
} }
viewModel.recodingIsStopped.value = false
viewModel.isRecording.value = true
viewModel.recordingIsPaused.value = false
}, },
onPauseRecording = { onPauseRecording = {
mediaRecorder.pause() mediaRecorder.pause()
viewModel.recodingIsStopped.value = false
viewModel.isRecording.value = true
viewModel.recordingIsPaused.value = true
}, },
onStopRecording = { onStopRecording = {
mediaRecorder.stop() mediaRecorder.stop()
val audioUri = outputFile.toUri() val audioUri = recordOutputFile.toUri()
mediaController.setMediaItem( mediaController.setMediaItem(
MediaItem.Builder() MediaItem.Builder()
.setUri(audioUri) .setUri(audioUri)
@ -333,14 +392,20 @@ class CreatePostActivity : IsolaattiBaseActivity() {
.build() .build()
) )
viewModel.audioToUpload = audioUri viewModel.audioToUpload = audioUri
viewModel.recodingIsStopped.value = true
viewModel.isRecording.value = false
viewModel.recordingIsPaused.value = false
}, },
isPlaying = isPlaying, isPlaying = isPlaying,
position = position, position = position,
durationSeconds = duration, durationSeconds = duration,
isRecording = false, isRecording = isRecording,
recordIsPaused = false, recordIsPaused = recordIsPaused,
recordIsStopped = false, recordIsStopped = recordIsStopped,
isLoading = isLoading isLoading = isLoading,
onSeek = { ms ->
mediaController.seekTo(ms.toLong())
}
) )
} }