From b3bb43367a03a02647486e9cd4762728ab715935 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:57:42 +0100 Subject: [PATCH] feat: Use VideoBatchesFolder and AudioBatchesFolder --- .../alibi/helpers/AudioBatchesFolder.kt | 71 +++++++++++++++++++ .../myzel394/alibi/helpers/BatchesFolder.kt | 66 ++++------------- .../myzel394/alibi/helpers/MediaConverter.kt | 58 ++++++++++++++- .../alibi/helpers/VideoBatchesFolder.kt | 71 +++++++++++++++++++ .../alibi/services/VideoRecorderService.kt | 1 + .../alibi/ui/screens/AudioRecorderScreen.kt | 6 +- 6 files changed, 216 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/helpers/AudioBatchesFolder.kt create mode 100644 app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt diff --git a/app/src/main/java/app/myzel394/alibi/helpers/AudioBatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/AudioBatchesFolder.kt new file mode 100644 index 0000000..47df507 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/helpers/AudioBatchesFolder.kt @@ -0,0 +1,71 @@ +package app.myzel394.alibi.helpers + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import com.arthenica.ffmpegkit.FFmpegKitConfig +import java.time.LocalDateTime + +class AudioBatchesFolder( + override val context: Context, + override val type: BatchType, + override val customFolder: DocumentFile? = null, + override val subfolderName: String = ".recordings", +) : BatchesFolder( + context, + type, + customFolder, + subfolderName, +) { + override fun getOutputFileForFFmpeg( + date: LocalDateTime, + extension: String, + ): String { + return when (type) { + BatchType.INTERNAL -> asInternalGetOutputFile(date, extension).absolutePath + BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite( + context, + customFolder!!.createFile( + "audio/${extension}", + getName(date, extension), + )!!.uri + )!! + } + } + + override suspend fun concatenate( + recordingStart: LocalDateTime, + extension: String, + disableCache: Boolean, + ) { + if (!disableCache && checkIfOutputAlreadyExists(recordingStart, extension)) { + return + } + + val filePaths = getBatchesForFFmpeg() + val outputFile = getOutputFileForFFmpeg( + date = recordingStart, + extension = extension, + ) + + MediaConverter.concatenateAudioFiles( + inputFiles = filePaths, + outputFile = outputFile, + ).await() + } + + companion object { + fun viaInternalFolder(context: Context): BatchesFolder { + return AudioBatchesFolder(context, BatchType.INTERNAL) + } + + fun viaCustomFolder(context: Context, folder: DocumentFile): BatchesFolder { + return AudioBatchesFolder(context, BatchType.CUSTOM, folder) + } + + fun importFromFolder(folder: String, context: Context): BatchesFolder = when (folder) { + "_'internal" -> viaInternalFolder(context) + else -> viaCustomFolder(context, DocumentFile.fromTreeUri(context, Uri.parse(folder))!!) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt index d40d575..234c83a 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/BatchesFolder.kt @@ -11,11 +11,11 @@ import android.net.Uri import android.os.ParcelFileDescriptor import java.io.FileDescriptor -data class BatchesFolder( - val context: Context, - val type: BatchType, - val customFolder: DocumentFile? = null, - val subfolderName: String = ".recordings", +abstract class BatchesFolder( + open val context: Context, + open val type: BatchType, + open val customFolder: DocumentFile? = null, + open val subfolderName: String = ".recordings", ) { private var customFileFileDescriptor: ParcelFileDescriptor? = null @@ -24,7 +24,7 @@ data class BatchesFolder( BatchType.INTERNAL -> getFolder(context).mkdirs() BatchType.CUSTOM -> { if (customFolder!!.findFile(subfolderName) == null) { - customFolder.createDirectory(subfolderName) + customFolder!!.createDirectory(subfolderName) } } } @@ -89,22 +89,6 @@ data class BatchesFolder( return getCustomDefinedFolder().createFile("audio/$extension", getName(date, extension))!! } - fun getOutputFileForFFmpeg( - date: LocalDateTime, - extension: String, - ): String { - return when (type) { - BatchType.INTERNAL -> asInternalGetOutputFile(date, extension).absolutePath - BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite( - context, - customFolder!!.createFile( - "audio/${extension}", - getName(date, extension), - )!!.uri - )!! - } - } - fun checkIfOutputAlreadyExists( date: LocalDateTime, extension: String @@ -122,27 +106,16 @@ data class BatchesFolder( } } - suspend fun exportToOneFile( + abstract fun getOutputFileForFFmpeg( + date: LocalDateTime, + extension: String, + ): String + + abstract suspend fun concatenate( recordingStart: LocalDateTime, extension: String, disableCache: Boolean = false, - ) { - if (!disableCache && checkIfOutputAlreadyExists(recordingStart, extension)) { - return - } - - val filePaths = getBatchesForFFmpeg() - val outputFile = getOutputFileForFFmpeg( - date = recordingStart, - extension = extension, - ) - - MediaConverter.concatenate( - inputFiles = filePaths, - outputFile = outputFile, - extraCommand = " -acodec copy" - ).await() - } + ) fun exportFolderForSettings(): String { return when (type) { @@ -218,20 +191,7 @@ data class BatchesFolder( } companion object { - fun viaInternalFolder(context: Context): BatchesFolder { - return BatchesFolder(context, BatchType.INTERNAL) - } - - fun viaCustomFolder(context: Context, folder: DocumentFile): BatchesFolder { - return BatchesFolder(context, BatchType.CUSTOM, folder) - } - fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME) - - fun importFromFolder(folder: String, context: Context): BatchesFolder = when (folder) { - "_'internal" -> viaInternalFolder(context) - else -> viaCustomFolder(context, DocumentFile.fromTreeUri(context, Uri.parse(folder))!!) - } } } diff --git a/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt b/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt index 865ccc0..ec2e465 100644 --- a/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt +++ b/app/src/main/java/app/myzel394/alibi/helpers/MediaConverter.kt @@ -1,13 +1,16 @@ package app.myzel394.alibi.helpers +import android.content.Context import android.util.Log import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode import kotlinx.coroutines.CompletableDeferred +import java.io.File +import java.util.UUID class MediaConverter { companion object { - fun concatenate( + fun concatenateAudioFiles( inputFiles: Iterable, outputFile: String, extraCommand: String = "", @@ -17,7 +20,9 @@ class MediaConverter { val filePathsConcatenated = inputFiles.joinToString("|") val command = "-protocol_whitelist saf,concat,content,file,subfile" + - " -i 'concat:$filePathsConcatenated' -y" + + " -i 'concat:$filePathsConcatenated'" + + " -y" + + " -acodec copy" + extraCommand + " $outputFile" @@ -43,5 +48,54 @@ class MediaConverter { return completer } + + private fun createTempFile(content: String): File { + val name = UUID.randomUUID().toString() + + return File.createTempFile("temp-$name", ".txt").apply { + writeText(content) + } + } + + fun concatenateVideoFiles( + inputFiles: Iterable, + outputFile: String, + extraCommand: String = "", + ): CompletableDeferred { + val completer = CompletableDeferred() + + val listFile = createTempFile(inputFiles.joinToString("\n", prefix = "file ")) + + val command = + "-protocol_whitelist saf,concat,content,file,subfile" + + " -f concat" + + " -y" + + " -i ${listFile.absolutePath}" + + " -c copy" + + extraCommand + + " $outputFile" + + FFmpegKit.executeAsync( + command + ) { session -> + 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, + ) + ) + + completer.completeExceptionally(Exception("Failed to concatenate videos")) + } else { + completer.complete(Unit) + } + } + + return completer + } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt b/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt new file mode 100644 index 0000000..f5ace45 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/helpers/VideoBatchesFolder.kt @@ -0,0 +1,71 @@ +package app.myzel394.alibi.helpers + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import com.arthenica.ffmpegkit.FFmpegKitConfig +import java.time.LocalDateTime + +class VideoBatchesFolder( + override val context: Context, + override val type: BatchesFolder.BatchType, + override val customFolder: DocumentFile? = null, + override val subfolderName: String = ".recordings", +) : BatchesFolder( + context, + type, + customFolder, + subfolderName, +) { + override fun getOutputFileForFFmpeg(date: LocalDateTime, extension: String): String { + return when (type) { + BatchType.INTERNAL -> asInternalGetOutputFile(date, extension).absolutePath + BatchType.CUSTOM -> FFmpegKitConfig.getSafParameterForWrite( + context, + customFolder!!.createFile( + "video/${extension}", + getName(date, extension), + )!!.uri + )!! + } + } + + override suspend fun concatenate( + recordingStart: LocalDateTime, + extension: String, + disableCache: Boolean + ) { + if (!disableCache && checkIfOutputAlreadyExists(recordingStart, extension)) { + return + } + + val filePaths = getBatchesForFFmpeg() + val outputFile = getOutputFileForFFmpeg( + date = recordingStart, + extension = extension, + ) + + MediaConverter.concatenateAudioFiles( + inputFiles = filePaths, + outputFile = outputFile, + ).await() + } + + companion object { + fun viaInternalFolder(context: Context): BatchesFolder { + return VideoBatchesFolder(context, BatchType.INTERNAL) + } + + fun viaCustomFolder(context: Context, folder: DocumentFile): BatchesFolder { + return VideoBatchesFolder(context, BatchType.CUSTOM, folder) + } + + fun importFromFolder(folder: String, context: Context): BatchesFolder = when (folder) { + "_'internal" -> AudioBatchesFolder.viaInternalFolder(context) + else -> AudioBatchesFolder.viaCustomFolder( + context, + DocumentFile.fromTreeUri(context, Uri.parse(folder))!! + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt index f18e62e..d8d23ec 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -11,6 +11,7 @@ import androidx.camera.video.Recorder import androidx.camera.video.Recording import androidx.camera.video.VideoCapture import androidx.core.content.ContextCompat +import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.RecordingInformation import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt index b95e361..09f0866 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorderScreen.kt @@ -44,6 +44,7 @@ import app.myzel394.alibi.ui.utils.rememberFileSaverDialog import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.helpers.AudioBatchesFolder import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.ui.effects.rememberSettings import app.myzel394.alibi.ui.models.AudioRecorderModel @@ -128,9 +129,10 @@ fun AudioRecorderScreen( val recording = audioRecorder.recorderService?.getRecordingInformation() ?: settings.lastRecording ?: throw Exception("No recording information available") - val batchesFolder = BatchesFolder.importFromFolder(recording.folderPath, context) + val batchesFolder = + AudioBatchesFolder.importFromFolder(recording.folderPath, context) - batchesFolder.exportToOneFile( + batchesFolder.concatenate( recording.recordingStart, recording.fileExtension, )