WIP grabador en compose
This commit is contained in:
parent
77d1cc13b4
commit
ca80dfab2a
@ -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()}")
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user