Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simple audio / video trimmer. #178

Merged
merged 4 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
185 changes: 185 additions & 0 deletions app/src/main/java/com/bnyro/recorder/ui/components/PlayerController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package com.bnyro.recorder.ui.components

import android.text.format.DateUtils
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

@Composable
fun PlayerController(exoPlayer: ExoPlayer) {
with(exoPlayer) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
val positionAndDuration by positionAndDurationState()
Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
Text(DateUtils.formatElapsedTime(positionAndDuration.first / 1000))
var tempSliderPosition by remember { mutableStateOf<Float?>(null) }
Slider(
modifier = Modifier.weight(1f),
value = tempSliderPosition ?: positionAndDuration.first.toFloat(),
onValueChange = { tempSliderPosition = it },
valueRange = 0f.rangeTo(
positionAndDuration.second?.toFloat() ?: Float.MAX_VALUE
),
onValueChangeFinished = {
tempSliderPosition?.let {
exoPlayer.seekTo(it.toLong())
}
tempSliderPosition = null
}
)
Text(
positionAndDuration.second?.let { DateUtils.formatElapsedTime(it / 1000) }
?: ""
)
}
ElevatedCard(
colors = CardDefaults.elevatedCardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
),
shape = CircleShape
) {
val playState by isPlayingState()
IconButton(
onClick = {
playPause()
}
) {
when (playState) {
PlayerState.Buffer -> {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
}

PlayerState.Play -> {
Icon(
Icons.Default.Pause,
contentDescription = "Pause"
SuhasDissa marked this conversation as resolved.
Show resolved Hide resolved
)
}

PlayerState.Pause -> {
Icon(
Icons.Default.PlayArrow,
contentDescription = "Play"
SuhasDissa marked this conversation as resolved.
Show resolved Hide resolved
)
}
}
}
}
}
}
}

@Composable
fun Player.isPlayingState(): State<PlayerState> {
return produceState(
initialValue = if (isPlaying) {
PlayerState.Play
} else {
PlayerState.Pause
},
this
) {
val listener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
playbackState
value = if (isPlaying) {
PlayerState.Play
} else {
PlayerState.Pause
}
}
}
addListener(listener)
if (!isActive) {
removeListener(listener)
Bnyro marked this conversation as resolved.
Show resolved Hide resolved
}
SuhasDissa marked this conversation as resolved.
Show resolved Hide resolved
}
}

@Composable
fun Player.positionAndDurationState(): State<Pair<Long, Long?>> {
return produceState(
initialValue = (currentPosition to duration.let { if (it < 0) null else it }),
this
) {
var isSeeking = false
val listener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_READY) {
isSeeking = false
}
}

override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
value = currentPosition to value.second
}

override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
isSeeking = true
value = currentPosition to duration.let { if (it < 0) null else it }
}
}
}
addListener(listener)

val pollJob = launch {
while (isActive) {
delay(1000)
if (!isSeeking) {
value = currentPosition to duration.let { if (it < 0) null else it }
}
}
}
if (!isActive) {
pollJob.cancel()
removeListener(listener)
}
}
}

fun Player.playPause() {
if (isPlaying) pause() else play()
}

enum class PlayerState {
SuhasDissa marked this conversation as resolved.
Show resolved Hide resolved
Buffer, Play, Pause
SuhasDissa marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fun RecordingItem(
recordingItem: RecordingItemData,
isSelected: Boolean,
onClick: (wasLongClick: Boolean) -> Unit,
onEdit: () -> Unit,
startPlayingAudio: () -> Unit
) {
val playerModel: PlayerModel = viewModel(factory = PlayerModel.Factory)
Expand Down Expand Up @@ -153,6 +154,15 @@ fun RecordingItem(
showDropDown = false
}
)
DropdownMenuItem(
text = {
Text(stringResource(R.string.trim))
},
onClick = {
onEdit.invoke()
showDropDown = false
}
)
DropdownMenuItem(
text = {
Text(stringResource(R.string.share))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@ import androidx.compose.material.icons.filled.VideoFile
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.viewmodel.compose.viewModel
import com.bnyro.recorder.R
import com.bnyro.recorder.obj.RecordingItemData
import com.bnyro.recorder.ui.models.PlayerModel
import com.bnyro.recorder.ui.screens.TrimmerScreen

@Composable
fun RecordingItemList(
Expand All @@ -34,6 +40,8 @@ fun RecordingItemList(
) {
val context = LocalContext.current
val icon = if (isVideoList) Icons.Default.VideoFile else Icons.Default.AudioFile
var chosenFile by remember { mutableStateOf<DocumentFile?>(null) }
var showTrimmer by remember { mutableStateOf(false) }
if (items.isNotEmpty()) {
Column {
LazyColumn(
Expand All @@ -56,6 +64,10 @@ fun RecordingItemList(
}
}
}
},
onEdit = {
chosenFile = it.recordingFile
showTrimmer = true
}
) {
playerModel.startPlaying(context, it.recordingFile)
Expand Down Expand Up @@ -89,4 +101,7 @@ fun RecordingItemList(
}
}
}
if (showTrimmer && chosenFile != null) {
TrimmerScreen(onDismissRequest = { showTrimmer = false }, inputFile = chosenFile!!)
}
}
52 changes: 52 additions & 0 deletions app/src/main/java/com/bnyro/recorder/ui/models/TrimmerModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.bnyro.recorder.ui.models

import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.bnyro.recorder.App
import com.bnyro.recorder.util.MediaTrimmer
import com.google.android.exoplayer2.ExoPlayer
import kotlinx.coroutines.launch

class TrimmerModel(context: Context) : ViewModel() {

val player = ExoPlayer.Builder(context).build()

var startTimeStamp by mutableLongStateOf(0L)
var endTimeStamp by mutableStateOf<Long?>(null)

fun startTrimmer(context: Context, inputFile: DocumentFile) {
SuhasDissa marked this conversation as resolved.
Show resolved Hide resolved
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
viewModelScope.launch {
val trimmer = MediaTrimmer()
Toast.makeText(context, "Starting Trimmer", Toast.LENGTH_LONG).show()
val result = trimmer.trimMedia(context, inputFile, startTimeStamp, endTimeStamp!!)
if (result) {
Toast.makeText(context, "Trim Success", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(context, "Trim Failed", Toast.LENGTH_LONG).show()
}
}
}
}

companion object {
val Factory = viewModelFactory {
initializer {
val application =
(this[APPLICATION_KEY] as App)
TrimmerModel(application)
}
}
}
}
Loading
Loading