fix: Fix mainly Audio recording and some bugfixes for video recording

This commit is contained in:
Myzel394 2023-12-31 22:10:14 +01:00
parent fa72ee096e
commit f4334bf26b
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
7 changed files with 146 additions and 59 deletions

View File

@ -2,12 +2,16 @@ package app.myzel394.alibi.helpers
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.net.toFile import androidx.annotation.RequiresApi
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import app.myzel394.alibi.helpers.MediaConverter.Companion.concatenateVideoFiles import app.myzel394.alibi.helpers.MediaConverter.Companion.concatenateAudioFiles
import app.myzel394.alibi.helpers.VideoBatchesFolder.Companion.MEDIA_SUBFOLDER import app.myzel394.alibi.ui.MEDIA_SUBFOLDER_NAME
import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.FFmpegKitConfig
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
@ -24,15 +28,17 @@ class AudioBatchesFolder(
customFolder, customFolder,
subfolderName, subfolderName,
) { ) {
override val concatenationFunction = ::concatenateVideoFiles override val concatenationFunction = ::concatenateAudioFiles
override val ffmpegParameters = FFMPEG_PARAMETERS override val ffmpegParameters = FFMPEG_PARAMETERS
override val scopedMediaContentUri: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI override val scopedMediaContentUri: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
override val legacyMediaFolder = File( override val legacyMediaFolder = File(
scopedMediaContentUri.toFile(), // TODO: Add support for `DIRECTORY_RECORDINGS`
MEDIA_SUBFOLDER + "/" + subfolderName Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
MEDIA_RECORDINGS_SUBFOLDER,
) )
private var customFileFileDescriptor: ParcelFileDescriptor? = null private var customFileFileDescriptor: ParcelFileDescriptor? = null
private var mediaFileFileDescriptor: ParcelFileDescriptor? = null
override fun getOutputFileForFFmpeg( override fun getOutputFileForFFmpeg(
date: LocalDateTime, date: LocalDateTime,
@ -54,7 +60,28 @@ class AudioBatchesFolder(
} }
BatchType.MEDIA -> { BatchType.MEDIA -> {
return "" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val mediaUri = getOrCreateMediaFile(
name = getName(date, extension),
mimeType = "audio/$extension",
relativePath = Environment.DIRECTORY_DCIM + "/" + MEDIA_SUBFOLDER_NAME,
)
return FFmpegKitConfig.getSafParameterForWrite(
context,
mediaUri
)!!
} else {
val path = arrayOf(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
MEDIA_SUBFOLDER_NAME,
getName(date, extension)
).joinToString("/")
return File(path)
.apply {
createNewFile()
}.absolutePath
}
} }
} }
} }
@ -63,6 +90,9 @@ class AudioBatchesFolder(
runCatching { runCatching {
customFileFileDescriptor?.close() customFileFileDescriptor?.close()
} }
runCatching {
mediaFileFileDescriptor?.close()
}
} }
fun asCustomGetFileDescriptor( fun asCustomGetFileDescriptor(
@ -81,17 +111,44 @@ class AudioBatchesFolder(
return customFileFileDescriptor!!.fileDescriptor return customFileFileDescriptor!!.fileDescriptor
} }
@RequiresApi(Build.VERSION_CODES.Q)
fun asMediaGetScopedStorageFileDescriptor(
name: String,
mimeType: String
): FileDescriptor {
runCatching {
mediaFileFileDescriptor?.close()
}
val mediaUri = getOrCreateMediaFile(
name = name,
mimeType = mimeType,
relativePath = SCOPED_STORAGE_RELATIVE_PATH,
)
mediaFileFileDescriptor = context.contentResolver.openFileDescriptor(mediaUri, "w")!!
return mediaFileFileDescriptor!!.fileDescriptor
}
companion object { companion object {
fun viaInternalFolder(context: Context) = AudioBatchesFolder(context, BatchType.INTERNAL) fun viaInternalFolder(context: Context) = AudioBatchesFolder(context, BatchType.INTERNAL)
fun viaCustomFolder(context: Context, folder: DocumentFile) = fun viaCustomFolder(context: Context, folder: DocumentFile) =
AudioBatchesFolder(context, BatchType.CUSTOM, folder) AudioBatchesFolder(context, BatchType.CUSTOM, folder)
fun viaMediaFolder(context: Context) = AudioBatchesFolder(context, BatchType.MEDIA)
fun importFromFolder(folder: String, context: Context) = when (folder) { fun importFromFolder(folder: String, context: Context) = when (folder) {
"_'internal" -> viaInternalFolder(context) RECORDER_INTERNAL_SELECTED_VALUE -> viaInternalFolder(context)
RECORDER_MEDIA_SELECTED_VALUE -> viaMediaFolder(context)
else -> viaCustomFolder(context, DocumentFile.fromTreeUri(context, Uri.parse(folder))!!) else -> viaCustomFolder(context, DocumentFile.fromTreeUri(context, Uri.parse(folder))!!)
} }
val MEDIA_RECORDINGS_SUBFOLDER = MEDIA_SUBFOLDER_NAME + "/audio_recordings"
val SCOPED_STORAGE_RELATIVE_PATH =
Environment.DIRECTORY_DCIM + "/" + MEDIA_RECORDINGS_SUBFOLDER
// Parameters to be passed in descending order // Parameters to be passed in descending order
// Those parameters first try to concatenate without re-encoding // Those parameters first try to concatenate without re-encoding
// if that fails, it'll try several fallback methods // if that fails, it'll try several fallback methods

View File

@ -170,6 +170,14 @@ abstract class BatchesFolder(
return File(getInternalFolder(), getName(date, extension)) return File(getInternalFolder(), getName(date, extension))
} }
fun asMediaGetLegacyFile(name: String): File = File(
legacyMediaFolder,
name
).apply {
createNewFile()
}
fun checkIfOutputAlreadyExists( fun checkIfOutputAlreadyExists(
date: LocalDateTime, date: LocalDateTime,
extension: String extension: String

View File

@ -1,21 +1,20 @@
package app.myzel394.alibi.helpers package app.myzel394.alibi.helpers
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.net.toFile
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import app.myzel394.alibi.helpers.MediaConverter.Companion.concatenateVideoFiles import app.myzel394.alibi.helpers.MediaConverter.Companion.concatenateVideoFiles
import app.myzel394.alibi.ui.MEDIA_SUBFOLDER_NAME
import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.FFmpegKitConfig
import java.io.File import java.io.File
import java.nio.file.Paths
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.io.path.Path
class VideoBatchesFolder( class VideoBatchesFolder(
override val context: Context, override val context: Context,
@ -28,6 +27,7 @@ class VideoBatchesFolder(
customFolder, customFolder,
subfolderName, subfolderName,
) { ) {
// TODO: Sort batches!
override val concatenationFunction = ::concatenateVideoFiles override val concatenationFunction = ::concatenateVideoFiles
override val ffmpegParameters = FFMPEG_PARAMETERS override val ffmpegParameters = FFMPEG_PARAMETERS
override val scopedMediaContentUri: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI override val scopedMediaContentUri: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
@ -59,7 +59,7 @@ class VideoBatchesFolder(
val mediaUri = getOrCreateMediaFile( val mediaUri = getOrCreateMediaFile(
name = getName(date, extension), name = getName(date, extension),
mimeType = "video/$extension", mimeType = "video/$extension",
relativePath = Environment.DIRECTORY_DCIM + MEDIA_SUBFOLDER, relativePath = Environment.DIRECTORY_DCIM + "/" + MEDIA_SUBFOLDER_NAME,
) )
return FFmpegKitConfig.getSafParameterForWrite( return FFmpegKitConfig.getSafParameterForWrite(
@ -67,12 +67,12 @@ class VideoBatchesFolder(
mediaUri mediaUri
)!! )!!
} else { } else {
return Paths.get( val path = arrayOf(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI.path, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
Environment.DIRECTORY_DCIM, MEDIA_SUBFOLDER_NAME,
MEDIA_SUBFOLDER,
getName(date, extension) getName(date, extension)
).toFile() ).joinToString("/")
return File(path)
.apply { .apply {
createNewFile() createNewFile()
}.absolutePath }.absolutePath
@ -109,6 +109,24 @@ class VideoBatchesFolder(
} }
} }
fun asMediaGetScopedStorageContentValues(name: String) = ContentValues().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(
MediaStore.Video.Media.IS_PENDING,
1
)
put(
MediaStore.Video.Media.RELATIVE_PATH,
SCOPED_STORAGE_RELATIVE_PATH,
)
}
put(
MediaStore.Video.Media.DISPLAY_NAME,
name
)
}
companion object { companion object {
fun viaInternalFolder(context: Context) = VideoBatchesFolder(context, BatchType.INTERNAL) fun viaInternalFolder(context: Context) = VideoBatchesFolder(context, BatchType.INTERNAL)
@ -126,9 +144,9 @@ class VideoBatchesFolder(
) )
} }
val MEDIA_SUBFOLDER = "/alibi" val MEDIA_RECORDINGS_SUBFOLDER = MEDIA_SUBFOLDER_NAME + "/video_recordings"
val MEDIA_RECORDINGS_SUBFOLDER = MEDIA_SUBFOLDER + "/video_recordings" val SCOPED_STORAGE_RELATIVE_PATH =
val SCOPED_STORAGE_RELATIVE_PATH = Environment.DIRECTORY_DCIM + MEDIA_RECORDINGS_SUBFOLDER Environment.DIRECTORY_DCIM + "/" + MEDIA_RECORDINGS_SUBFOLDER
// Parameters to be passed in descending order // Parameters to be passed in descending order
// Those parameters first try to concatenate without re-encoding // Those parameters first try to concatenate without re-encoding

View File

@ -16,6 +16,7 @@ import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.helpers.AudioBatchesFolder import app.myzel394.alibi.helpers.AudioBatchesFolder
import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import app.myzel394.alibi.ui.utils.MicrophoneInfo import app.myzel394.alibi.ui.utils.MicrophoneInfo
import java.lang.IllegalStateException import java.lang.IllegalStateException
@ -160,6 +161,9 @@ class AudioRecorderService :
} }
} }
private fun getNameForMediaFile() =
"${batchesFolder.mediaPrefix}$counter.${settings.videoRecorderSettings.fileExtension}"
// ==== Actual recording related ==== // ==== Actual recording related ====
private fun createRecorder(): MediaRecorder { private fun createRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -196,8 +200,22 @@ class AudioRecorderService :
) )
} }
// TODO: Add media BatchesFolder.BatchType.MEDIA -> {
else -> {} if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setOutputFile(
batchesFolder.asMediaGetScopedStorageFileDescriptor(
getNameForMediaFile(),
"audio/${audioSettings.fileExtension}"
)
)
} else {
val name = getNameForMediaFile()
val file = batchesFolder.asMediaGetLegacyFile(name)
// TODO: Ask permission on settings screen
setOutputFile(file.absolutePath)
}
}
} }
setOutputFormat(audioSettings.getOutputFormat()) setOutputFormat(audioSettings.getOutputFormat())

View File

@ -1,11 +1,9 @@
package app.myzel394.alibi.services package app.myzel394.alibi.services
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.provider.MediaStore
import android.util.Range import android.util.Range
import androidx.camera.core.Camera import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
@ -35,7 +33,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import kotlin.properties.Delegates import kotlin.properties.Delegates
class VideoRecorderService : class VideoRecorderService :
@ -246,45 +243,30 @@ class VideoRecorderService :
) )
} else if (batchesFolder.type == BatchesFolder.BatchType.MEDIA) { } else if (batchesFolder.type == BatchesFolder.BatchType.MEDIA) {
if (SUPPORTS_SCOPED_STORAGE) { if (SUPPORTS_SCOPED_STORAGE) {
val name = getNameForMediaFile()
it.prepareRecording( it.prepareRecording(
this, this,
MediaStoreOutputOptions.Builder( MediaStoreOutputOptions
contentResolver, .Builder(
batchesFolder.scopedMediaContentUri, contentResolver,
).setContentValues( batchesFolder.scopedMediaContentUri,
ContentValues().apply { )
val name = getNameForMediaFile() .setContentValues(
batchesFolder.asMediaGetScopedStorageContentValues(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(
MediaStore.Video.Media.IS_PENDING,
1
)
put(
MediaStore.Video.Media.RELATIVE_PATH,
VideoBatchesFolder.SCOPED_STORAGE_RELATIVE_PATH,
)
}
put(
MediaStore.Video.Media.DISPLAY_NAME,
name name
) )
} )
).build() .build()
) )
} else { } else {
val name = getNameForMediaFile() val name = getNameForMediaFile()
val file = File(
batchesFolder.legacyMediaFolder,
name
).apply {
createNewFile()
}
it.prepareRecording( it.prepareRecording(
this, this,
FileOutputOptions.Builder(file).build() FileOutputOptions
.Builder(batchesFolder.asMediaGetLegacyFile(name))
.build()
) )
} }
} else { } else {

View File

@ -9,7 +9,8 @@ val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val SHEET_BOTTOM_OFFSET = 56.dp val SHEET_BOTTOM_OFFSET = 56.dp
val MAX_AMPLITUDE = 20000 val MAX_AMPLITUDE = 20000
val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val RECORDER_SUBFOLDER_NAME = ".recordings"
val MEDIA_SUBFOLDER_NAME = "alibi"
// TODO: Fix! // TODO: Fix!
val SUPPORTS_SCOPED_STORAGE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O val SUPPORTS_SCOPED_STORAGE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O

View File

@ -10,7 +10,9 @@ import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.RecordingInformation import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.helpers.AudioBatchesFolder import app.myzel394.alibi.helpers.AudioBatchesFolder
import app.myzel394.alibi.helpers.VideoBatchesFolder
import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import app.myzel394.alibi.ui.utils.MicrophoneInfo import app.myzel394.alibi.ui.utils.MicrophoneInfo
class AudioRecorderModel : class AudioRecorderModel :
@ -63,16 +65,17 @@ class AudioRecorderModel :
} }
override fun startRecording(context: Context, settings: AppSettings) { override fun startRecording(context: Context, settings: AppSettings) {
batchesFolder = if (settings.saveFolder == null) batchesFolder = when (settings.saveFolder) {
AudioBatchesFolder.viaInternalFolder(context) null -> AudioBatchesFolder.viaInternalFolder(context)
else RECORDER_MEDIA_SELECTED_VALUE -> AudioBatchesFolder.viaMediaFolder(context)
AudioBatchesFolder.viaCustomFolder( else -> AudioBatchesFolder.viaCustomFolder(
context, context,
DocumentFile.fromTreeUri( DocumentFile.fromTreeUri(
context, context,
Uri.parse(settings.saveFolder) Uri.parse(settings.saveFolder)
)!! )!!
) )
}
super.startRecording(context, settings) super.startRecording(context, settings)
} }