mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-19 07:15:25 +02:00
feat: Use VideoBatchesFolder and AudioBatchesFolder
This commit is contained in:
parent
3d17012fb7
commit
b3bb43367a
@ -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))!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,11 +11,11 @@ import android.net.Uri
|
|||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import java.io.FileDescriptor
|
import java.io.FileDescriptor
|
||||||
|
|
||||||
data class BatchesFolder(
|
abstract class BatchesFolder(
|
||||||
val context: Context,
|
open val context: Context,
|
||||||
val type: BatchType,
|
open val type: BatchType,
|
||||||
val customFolder: DocumentFile? = null,
|
open val customFolder: DocumentFile? = null,
|
||||||
val subfolderName: String = ".recordings",
|
open val subfolderName: String = ".recordings",
|
||||||
) {
|
) {
|
||||||
private var customFileFileDescriptor: ParcelFileDescriptor? = null
|
private var customFileFileDescriptor: ParcelFileDescriptor? = null
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ data class BatchesFolder(
|
|||||||
BatchType.INTERNAL -> getFolder(context).mkdirs()
|
BatchType.INTERNAL -> getFolder(context).mkdirs()
|
||||||
BatchType.CUSTOM -> {
|
BatchType.CUSTOM -> {
|
||||||
if (customFolder!!.findFile(subfolderName) == null) {
|
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))!!
|
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(
|
fun checkIfOutputAlreadyExists(
|
||||||
date: LocalDateTime,
|
date: LocalDateTime,
|
||||||
extension: String
|
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,
|
recordingStart: LocalDateTime,
|
||||||
extension: String,
|
extension: String,
|
||||||
disableCache: Boolean = false,
|
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 {
|
fun exportFolderForSettings(): String {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
@ -218,20 +191,7 @@ data class BatchesFolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 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))!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
package app.myzel394.alibi.helpers
|
package app.myzel394.alibi.helpers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
import com.arthenica.ffmpegkit.FFmpegKit
|
||||||
import com.arthenica.ffmpegkit.ReturnCode
|
import com.arthenica.ffmpegkit.ReturnCode
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class MediaConverter {
|
class MediaConverter {
|
||||||
companion object {
|
companion object {
|
||||||
fun concatenate(
|
fun concatenateAudioFiles(
|
||||||
inputFiles: Iterable<String>,
|
inputFiles: Iterable<String>,
|
||||||
outputFile: String,
|
outputFile: String,
|
||||||
extraCommand: String = "",
|
extraCommand: String = "",
|
||||||
@ -17,7 +20,9 @@ class MediaConverter {
|
|||||||
val filePathsConcatenated = inputFiles.joinToString("|")
|
val filePathsConcatenated = inputFiles.joinToString("|")
|
||||||
val command =
|
val command =
|
||||||
"-protocol_whitelist saf,concat,content,file,subfile" +
|
"-protocol_whitelist saf,concat,content,file,subfile" +
|
||||||
" -i 'concat:$filePathsConcatenated' -y" +
|
" -i 'concat:$filePathsConcatenated'" +
|
||||||
|
" -y" +
|
||||||
|
" -acodec copy" +
|
||||||
extraCommand +
|
extraCommand +
|
||||||
" $outputFile"
|
" $outputFile"
|
||||||
|
|
||||||
@ -43,5 +48,54 @@ class MediaConverter {
|
|||||||
|
|
||||||
return completer
|
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<String>,
|
||||||
|
outputFile: String,
|
||||||
|
extraCommand: String = "",
|
||||||
|
): CompletableDeferred<Unit> {
|
||||||
|
val completer = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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))!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import androidx.camera.video.Recorder
|
|||||||
import androidx.camera.video.Recording
|
import androidx.camera.video.Recording
|
||||||
import androidx.camera.video.VideoCapture
|
import androidx.camera.video.VideoCapture
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import app.myzel394.alibi.db.AppSettings
|
||||||
import app.myzel394.alibi.db.RecordingInformation
|
import app.myzel394.alibi.db.RecordingInformation
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -44,6 +44,7 @@ import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
|
|||||||
import app.myzel394.alibi.R
|
import app.myzel394.alibi.R
|
||||||
import app.myzel394.alibi.dataStore
|
import app.myzel394.alibi.dataStore
|
||||||
import app.myzel394.alibi.db.AppSettings
|
import app.myzel394.alibi.db.AppSettings
|
||||||
|
import app.myzel394.alibi.helpers.AudioBatchesFolder
|
||||||
import app.myzel394.alibi.helpers.BatchesFolder
|
import app.myzel394.alibi.helpers.BatchesFolder
|
||||||
import app.myzel394.alibi.ui.effects.rememberSettings
|
import app.myzel394.alibi.ui.effects.rememberSettings
|
||||||
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
import app.myzel394.alibi.ui.models.AudioRecorderModel
|
||||||
@ -128,9 +129,10 @@ fun AudioRecorderScreen(
|
|||||||
val recording = audioRecorder.recorderService?.getRecordingInformation()
|
val recording = audioRecorder.recorderService?.getRecordingInformation()
|
||||||
?: settings.lastRecording
|
?: settings.lastRecording
|
||||||
?: throw Exception("No recording information available")
|
?: 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.recordingStart,
|
||||||
recording.fileExtension,
|
recording.fileExtension,
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user