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