Skip to content

Commit

Permalink
feat: Add progress bar support for video
Browse files Browse the repository at this point in the history
  • Loading branch information
Myzel394 committed Jan 3, 2024
1 parent 3f1e00a commit 386d3cb
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 92 deletions.
32 changes: 27 additions & 5 deletions app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import app.myzel394.alibi.ui.utils.PermissionHelper
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.FFprobeSession
import kotlinx.coroutines.CompletableDeferred
import kotlin.reflect.KFunction3
import kotlin.reflect.KFunction4

abstract class BatchesFolder(
open val context: Context,
open val type: BatchType,
open val customFolder: DocumentFile? = null,
open val subfolderName: String = ".recordings",
) {
abstract val concatenationFunction: KFunction3<Iterable<String>, String, String, CompletableDeferred<Unit>>
abstract val concatenationFunction: KFunction4<Iterable<String>, String, String, (Int) -> Unit, CompletableDeferred<Unit>>
abstract val ffmpegParameters: Array<String>
abstract val scopedMediaContentUri: Uri
abstract val legacyMediaFolder: File
Expand Down Expand Up @@ -245,11 +247,13 @@ abstract class BatchesFolder(

abstract fun cleanup()

open suspend fun concatenate(
suspend fun concatenate(
recordingStart: LocalDateTime,
extension: String,
disableCache: Boolean = false,
onNextParameterTry: (String) -> Unit = {},
durationPerBatchInMilliseconds: Long = 0,
onProgress: (Float) -> Unit = {},
): String {
if (!disableCache && checkIfOutputAlreadyExists(recordingStart, extension)) {
return getOutputFileForFFmpeg(
Expand All @@ -264,6 +268,20 @@ abstract class BatchesFolder(

try {
val filePaths = getBatchesForFFmpeg()

// Casting here to float so it doesn't need to redo it on every progress update
var fullTime: Float? = null

runCatching {
// `fullTime` is not accurate as the last batch might be shorter,
// but it's good enough for the progress bar
val lastBatchTime = (FFprobeKit.execute(
"-i ${filePaths.last()} -show_entries format=duration -v quiet -of csv=\"p=0\"",
).output.toFloat() * 1000).toLong()
fullTime =
((durationPerBatchInMilliseconds * (filePaths.size - 1)) + lastBatchTime).toFloat()
}

val outputFile = getOutputFileForFFmpeg(
date = recordingStart,
extension = extension,
Expand All @@ -272,8 +290,12 @@ abstract class BatchesFolder(
concatenationFunction(
filePaths,
outputFile,
parameter,
).await()
parameter
) { time ->
if (fullTime != null) {
onProgress(time / fullTime!!)
}
}.await()
return outputFile
} catch (e: MediaConverter.FFmpegException) {
continue
Expand Down
113 changes: 94 additions & 19 deletions app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,79 @@ import com.arthenica.ffmpegkit.ReturnCode
import kotlinx.coroutines.CompletableDeferred
import java.io.File
import java.util.UUID
import kotlin.math.log

// Abstract class for concatenating audio and video files
// The concatenator runs in its own thread to avoid unresponsiveness.
// You may be wondering why we simply not iterate over the FFMPEG_PARAMETERS
// in this thread and then call each FFmpeg initiation just right after it?
// The answer: It's easier; We don't have to deal with the `getBatchesForFFmpeg` function, because
// the batches are only usable once and we if iterate in this thread over the FFMPEG_PARAMETERS
// we would need to refetch the batches here, which is more messy.
// This is okay, because in 99% of the time the first or second parameter will work,
// and so there is no real performance loss.
abstract class Concatenator(
private val inputFiles: Iterable<String>,
private val outputFile: String,
private val extraCommand: String
) : Thread() {
abstract fun concatenate(): CompletableDeferred<Unit>

class FFmpegException(message: String) : Exception(message)
}

data class AudioConcatenator(
private val inputFiles: Iterable<String>,
private val outputFile: String,
private val extraCommand: String
) : Concatenator(
inputFiles,
outputFile,
extraCommand
) {
override fun concatenate(): CompletableDeferred<Unit> {
val completer = CompletableDeferred<Unit>()

val filePathsConcatenated = inputFiles.joinToString("|")
val command =
"-protocol_whitelist saf,concat,content,file,subfile" +
" -i 'concat:$filePathsConcatenated'" +
" -y" +
extraCommand +
" $outputFile"

FFmpegKit.executeAsync(
command
) { session ->
if (!ReturnCode.isSuccess(session!!.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)

completer.completeExceptionally(Exception("Failed to concatenate audios"))
} else {
completer.complete(Unit)
}
}

return completer
}
}


class MediaConverter {
companion object {
fun concatenateAudioFiles(
inputFiles: Iterable<String>,
outputFile: String,
extraCommand: String = "",
onProgress: (Int) -> Unit = { },
): CompletableDeferred<Unit> {
val completer = CompletableDeferred<Unit>()

Expand Down Expand Up @@ -63,6 +129,7 @@ class MediaConverter {
inputFiles: Iterable<String>,
outputFile: String,
extraCommand: String = "",
onProgress: (Int) -> Unit = { },
): CompletableDeferred<Unit> {
val completer = CompletableDeferred<Unit>()

Expand All @@ -75,32 +142,40 @@ class MediaConverter {
" -i ${listFile.absolutePath}" +
extraCommand +
" -strict normal" +
// TODO: Check if those params work
" -nostats" +
" -loglevel error" +
" -y" +
" $outputFile"

FFmpegKit.executeAsync(
command
) { session ->
runCatching {
listFile.delete()
}

if (!ReturnCode.isSuccess(session!!.returnCode)) {
Log.d(
"Video Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
command,
{ session ->
runCatching {
listFile.delete()
}

if (ReturnCode.isSuccess(session!!.returnCode)) {
completer.complete(Unit)
} else {
Log.d(
"Video Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)
)

completer.completeExceptionally(FFmpegException("Failed to concatenate videos"))
} else {
completer.complete(Unit)
completer.completeExceptionally(FFmpegException("Failed to concatenate videos"))
}
},
{},
{ statistics ->
onProgress(statistics.time)
}
}
)

return completer
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import androidx.compose.ui.unit.dp
import app.myzel394.alibi.R

@Composable
fun RecorderProcessingDialog() {
fun RecorderProcessingDialog(
progress: Float?,
) {
AlertDialog(
onDismissRequest = { },
icon = {
Expand All @@ -39,7 +41,10 @@ fun RecorderProcessingDialog() {
stringResource(R.string.ui_recorder_action_save_processing_dialog_description),
)
Spacer(modifier = Modifier.height(32.dp))
LinearProgressIndicator()
if (progress != null)
LinearProgressIndicator(progress = progress)
else
LinearProgressIndicator()
}
},
confirmButton = {}
Expand Down
Loading

0 comments on commit 386d3cb

Please sign in to comment.