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
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.padding
import androidx.compose.material.icons.Icons
@ -13,6 +15,11 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
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.res.painterResource
import androidx.compose.ui.unit.dp
@ -32,9 +39,10 @@ fun AudioPlayer(
isLoading: Boolean,
durationMs: Long,
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) {
LinearProgressIndicator(modifier = Modifier.padding(horizontal = 4.dp).weight(1f))
} 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()}")

View File

@ -56,6 +56,7 @@ fun AudioRecorder(
isLoading: Boolean,
durationSeconds: Long,
position: Long,
onSeek: (position: Float) -> Unit = {},
modifier: Modifier = Modifier
) {
@ -67,9 +68,9 @@ fun AudioRecorder(
var duration by remember { mutableLongStateOf(0L) }
LaunchedEffect(isPlaying) {
LaunchedEffect(isRecording, recordIsPaused) {
coroutineScope {
while(isPlaying) {
while(isRecording && !recordIsPaused) {
duration += 500
delay(500.milliseconds)
}
@ -93,7 +94,7 @@ fun AudioRecorder(
when {
audioRecordPermissionState.status.isGranted -> {
when {
!isRecording -> {
!isRecording && !recordIsStopped-> {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
@ -112,7 +113,7 @@ fun AudioRecorder(
}
}
isRecording -> {
isRecording && !recordIsStopped -> {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 8.dp)
@ -121,9 +122,10 @@ fun AudioRecorder(
Spacer(Modifier.weight(1f))
IconButton(onClick = {
if(recordIsPaused) {
onPauseRecording()
} else {
onStartRecording(true)
} else {
onPauseRecording()
}
}) {
@ -141,7 +143,7 @@ fun AudioRecorder(
}
}
recordIsStopped -> {
else -> {
AudioPlayer(
modifier = Modifier.fillMaxWidth(),
positionMs = position,
@ -149,7 +151,8 @@ fun AudioRecorder(
isPlaying = isPlaying,
durationMs = durationSeconds,
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.onEach
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
@HiltViewModel
@ -68,6 +69,12 @@ class CreatePostViewModel @Inject constructor(
var ended: Boolean = false
// 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

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.presentation.CreatePostViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Calendar
import kotlin.time.Duration.Companion.milliseconds
@ -144,6 +146,10 @@ class CreatePostActivity : IsolaattiBaseActivity() {
}
}
private lateinit var recordOutputFile: File
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
@ -152,13 +158,27 @@ class CreatePostActivity : IsolaattiBaseActivity() {
val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java))
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(this) else MediaRecorder()
lifecycleScope.launch {
mediaController = mediaControllerFuture.await()
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 {
val text by viewModel.liveContent.observeAsState("")
val focusRequester = remember { FocusRequester() }
@ -257,6 +277,7 @@ class CreatePostActivity : IsolaattiBaseActivity() {
IconButton(
onClick = {
audioRecorderIsVisible = !audioRecorderIsVisible
mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(this@CreatePostActivity) else MediaRecorder()
}
) {
Icon(painterResource(id = R.drawable.baseline_mic_24), null)
@ -275,7 +296,9 @@ class CreatePostActivity : IsolaattiBaseActivity() {
val duration by viewModel.duration.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) {
while(isPlaying) {
@ -288,6 +311,29 @@ class CreatePostActivity : IsolaattiBaseActivity() {
modifier = Modifier.padding(16.dp),
onDismiss = {
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 = {
if(mediaController.isCommandAvailable(Player.COMMAND_PREPARE)) {
@ -302,28 +348,41 @@ class CreatePostActivity : IsolaattiBaseActivity() {
onPause = {
mediaController.pause()
},
onStartRecording = {
onStartRecording = { fromPause ->
mediaRecorder.run {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
if (Build.VERSION.SDK_INT >= 26) {
setOutputFile(outputFile)
} else {
setOutputFile(outputFile.path)
if(fromPause) {
mediaRecorder.resume()
} else {
mediaRecorder.run {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
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 = {
mediaRecorder.pause()
viewModel.recodingIsStopped.value = false
viewModel.isRecording.value = true
viewModel.recordingIsPaused.value = true
},
onStopRecording = {
mediaRecorder.stop()
val audioUri = outputFile.toUri()
val audioUri = recordOutputFile.toUri()
mediaController.setMediaItem(
MediaItem.Builder()
.setUri(audioUri)
@ -333,14 +392,20 @@ class CreatePostActivity : IsolaattiBaseActivity() {
.build()
)
viewModel.audioToUpload = audioUri
viewModel.recodingIsStopped.value = true
viewModel.isRecording.value = false
viewModel.recordingIsPaused.value = false
},
isPlaying = isPlaying,
position = position,
durationSeconds = duration,
isRecording = false,
recordIsPaused = false,
recordIsStopped = false,
isLoading = isLoading
isRecording = isRecording,
recordIsPaused = recordIsPaused,
recordIsStopped = recordIsStopped,
isLoading = isLoading,
onSeek = { ms ->
mediaController.seekTo(ms.toLong())
}
)
}