fix: Properly reconnect audio on app focus

This commit is contained in:
Myzel394 2023-12-16 22:13:38 +01:00
parent ff8ea3e1f2
commit cda0b7f195
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
10 changed files with 70 additions and 107 deletions

View File

@ -3,11 +3,13 @@ package app.myzel394.alibi.db
import android.content.Context import android.content.Context
import android.media.MediaRecorder import android.media.MediaRecorder
import android.os.Build import android.os.Build
import androidx.camera.video.FileOutputOptions
import androidx.camera.video.Quality import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector import androidx.camera.video.QualitySelector
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.helpers.AudioBatchesFolder import app.myzel394.alibi.helpers.AudioBatchesFolder
import app.myzel394.alibi.helpers.VideoBatchesFolder import app.myzel394.alibi.helpers.VideoBatchesFolder
import app.myzel394.alibi.services.VideoRecorderService
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.LocalDateTime import java.time.LocalDateTime
@ -260,6 +262,19 @@ data class AudioRecorderSettings(
return supportedFormats.contains(outputFormat) return supportedFormats.contains(outputFormat)
} }
val fileExtension: String
get() = when (outputFormat) {
MediaRecorder.OutputFormat.AAC_ADTS -> "aac"
MediaRecorder.OutputFormat.THREE_GPP -> "3gp"
MediaRecorder.OutputFormat.MPEG_4 -> "mp4"
MediaRecorder.OutputFormat.MPEG_2_TS -> "ts"
MediaRecorder.OutputFormat.WEBM -> "webm"
MediaRecorder.OutputFormat.AMR_NB -> "amr"
MediaRecorder.OutputFormat.AMR_WB -> "awb"
MediaRecorder.OutputFormat.OGG -> "ogg"
else -> "raw"
}
companion object { companion object {
fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings() fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings()
val EXAMPLE_MAX_DURATIONS = listOf( val EXAMPLE_MAX_DURATIONS = listOf(
@ -403,6 +418,9 @@ data class VideoRecorderSettings(
fun getMimeType() = "video/mp4" fun getMimeType() = "video/mp4"
val fileExtension
get() = "mp4"
companion object { companion object {
fun getDefaultInstance() = VideoRecorderSettings() fun getDefaultInstance() = VideoRecorderSettings()

View File

@ -13,7 +13,6 @@ import android.os.Looper
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import app.myzel394.alibi.NotificationHelper import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.AudioRecorderSettings
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
@ -22,7 +21,7 @@ import app.myzel394.alibi.ui.utils.MicrophoneInfo
import java.lang.IllegalStateException import java.lang.IllegalStateException
class AudioRecorderService : class AudioRecorderService :
IntervalRecorderService<AudioRecorderService.Settings, RecordingInformation>() { IntervalRecorderService<RecordingInformation>() {
override var batchesFolder: BatchesFolder = AudioBatchesFolder.viaInternalFolder(this) override var batchesFolder: BatchesFolder = AudioBatchesFolder.viaInternalFolder(this)
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@ -169,6 +168,8 @@ class AudioRecorderService :
} else { } else {
MediaRecorder() MediaRecorder()
}.apply { }.apply {
val audioSettings = settings.audioRecorderSettings
// Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro // Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro
// and Redmi Buds 3 Pro: // and Redmi Buds 3 Pro:
// - MIC: Uses the bottom microphone of the phone (17) // - MIC: Uses the bottom microphone of the phone (17)
@ -180,22 +181,25 @@ class AudioRecorderService :
when (batchesFolder.type) { when (batchesFolder.type) {
BatchesFolder.BatchType.INTERNAL -> { BatchesFolder.BatchType.INTERNAL -> {
setOutputFile( setOutputFile(
batchesFolder.asInternalGetOutputPath(counter, settings.fileExtension) batchesFolder.asInternalGetOutputPath(counter, audioSettings.fileExtension)
) )
} }
BatchesFolder.BatchType.CUSTOM -> { BatchesFolder.BatchType.CUSTOM -> {
setOutputFile( setOutputFile(
batchesFolder.asCustomGetFileDescriptor(counter, settings.fileExtension) batchesFolder.asCustomGetFileDescriptor(
counter,
audioSettings.fileExtension
)
) )
} }
} }
setOutputFormat(settings.outputFormat) setOutputFormat(audioSettings.getOutputFormat())
setAudioEncoder(settings.encoder) setAudioEncoder(audioSettings.getEncoder())
setAudioEncodingBitRate(settings.bitRate) setAudioEncodingBitRate(audioSettings.bitRate)
setAudioSamplingRate(settings.samplingRate) setAudioSamplingRate(audioSettings.getSamplingRate())
setOnErrorListener(OnErrorListener { _, _, _ -> setOnErrorListener(OnErrorListener { _, _, _ ->
onError() onError()
}) })
@ -281,47 +285,8 @@ class AudioRecorderService :
folderPath = batchesFolder.exportFolderForSettings(), folderPath = batchesFolder.exportFolderForSettings(),
recordingStart = recordingStart, recordingStart = recordingStart,
maxDuration = settings.maxDuration, maxDuration = settings.maxDuration,
fileExtension = settings.fileExtension, fileExtension = settings.audioRecorderSettings.fileExtension,
intervalDuration = settings.intervalDuration, intervalDuration = settings.intervalDuration,
type = RecordingInformation.Type.AUDIO, type = RecordingInformation.Type.AUDIO,
) )
data class Settings(
override val maxDuration: Long,
override val intervalDuration: Long,
val bitRate: Int,
val samplingRate: Int,
val outputFormat: Int,
val encoder: Int,
val folder: String? = null,
) : IntervalRecorderService.Settings(
maxDuration = maxDuration,
intervalDuration = intervalDuration
) {
val fileExtension: String
get() = when (outputFormat) {
MediaRecorder.OutputFormat.AAC_ADTS -> "aac"
MediaRecorder.OutputFormat.THREE_GPP -> "3gp"
MediaRecorder.OutputFormat.MPEG_4 -> "mp4"
MediaRecorder.OutputFormat.MPEG_2_TS -> "ts"
MediaRecorder.OutputFormat.WEBM -> "webm"
MediaRecorder.OutputFormat.AMR_NB -> "amr"
MediaRecorder.OutputFormat.AMR_WB -> "awb"
MediaRecorder.OutputFormat.OGG -> "ogg"
else -> "raw"
}
companion object {
fun from(appSettings: AppSettings): Settings {
return Settings(
intervalDuration = appSettings.intervalDuration,
maxDuration = appSettings.maxDuration,
bitRate = appSettings.audioRecorderSettings.bitRate,
samplingRate = appSettings.audioRecorderSettings.getSamplingRate(),
outputFormat = appSettings.audioRecorderSettings.getOutputFormat(),
encoder = appSettings.audioRecorderSettings.getEncoder(),
)
}
}
}
} }

View File

@ -1,16 +1,17 @@
package app.myzel394.alibi.services package app.myzel394.alibi.services
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.helpers.BatchesFolder
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
abstract class IntervalRecorderService<S : IntervalRecorderService.Settings, I> : abstract class IntervalRecorderService<I> :
RecorderService() { RecorderService() {
protected var counter = 0L protected var counter = 0L
private set private set
lateinit var settings: S lateinit var settings: AppSettings
private lateinit var cycleTimer: ScheduledExecutorService private lateinit var cycleTimer: ScheduledExecutorService

View File

@ -19,7 +19,6 @@ import androidx.camera.video.VideoRecordEvent
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import app.myzel394.alibi.NotificationHelper import app.myzel394.alibi.NotificationHelper
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.BatchesFolder import app.myzel394.alibi.helpers.BatchesFolder
@ -34,7 +33,7 @@ import kotlinx.coroutines.withTimeoutOrNull
import kotlin.properties.Delegates import kotlin.properties.Delegates
class VideoRecorderService : class VideoRecorderService :
IntervalRecorderService<VideoRecorderService.Settings, RecordingInformation>() { IntervalRecorderService<RecordingInformation>() {
override var batchesFolder: BatchesFolder = VideoBatchesFolder.viaInternalFolder(this) override var batchesFolder: BatchesFolder = VideoBatchesFolder.viaInternalFolder(this)
private val job = SupervisorJob() private val job = SupervisorJob()
@ -149,18 +148,22 @@ class VideoRecorderService :
} }
private fun buildRecorder() = Recorder.Builder() private fun buildRecorder() = Recorder.Builder()
.setQualitySelector(settings.quality) .setQualitySelector(
settings.videoRecorderSettings.getQualitySelector()
?: QualitySelector.from(Quality.HIGHEST)
)
.apply { .apply {
if (settings.targetVideoBitRate != null) { if (settings.videoRecorderSettings.targetedVideoBitRate != null) {
setTargetVideoEncodingBitRate(settings.targetVideoBitRate!!) setTargetVideoEncodingBitRate(settings.videoRecorderSettings.targetedVideoBitRate!!)
} }
} }
.build() .build()
private fun buildVideoCapture(recorder: Recorder) = VideoCapture.Builder(recorder) private fun buildVideoCapture(recorder: Recorder) = VideoCapture.Builder(recorder)
.apply { .apply {
if (settings.targetFrameRate != null) { val frameRate = settings.videoRecorderSettings.targetFrameRate
setTargetFrameRate(Range(settings.targetFrameRate!!, settings.targetFrameRate!!)) if (frameRate != null) {
setTargetFrameRate(Range(frameRate, frameRate))
} }
} }
.build() .build()
@ -218,7 +221,7 @@ class VideoRecorderService :
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun prepareVideoRecording() = private fun prepareVideoRecording() =
videoCapture!!.output videoCapture!!.output
.prepareRecording(this, settings.getOutputOptions(this)) .prepareRecording(this, getOutputOptions())
.run { .run {
if (enableAudio) { if (enableAudio) {
return@run withAudioEnabled() return@run withAudioEnabled()
@ -231,32 +234,14 @@ class VideoRecorderService :
folderPath = batchesFolder.exportFolderForSettings(), folderPath = batchesFolder.exportFolderForSettings(),
recordingStart = recordingStart, recordingStart = recordingStart,
maxDuration = settings.maxDuration, maxDuration = settings.maxDuration,
fileExtension = settings.fileExtension, fileExtension = settings.videoRecorderSettings.fileExtension,
intervalDuration = settings.intervalDuration, intervalDuration = settings.intervalDuration,
type = RecordingInformation.Type.VIDEO, type = RecordingInformation.Type.VIDEO,
) )
companion object { fun getOutputOptions(): FileOutputOptions {
const val CAMERA_CLOSE_TIMEOUT = 20000L val fileName = "${counter}.${settings.videoRecorderSettings.fileExtension}"
} val file = batchesFolder.getInternalFolder().resolve(fileName).apply {
data class Settings(
override val maxDuration: Long,
override val intervalDuration: Long,
val folder: String? = null,
val targetVideoBitRate: Int? = null,
val targetFrameRate: Int? = null,
val quality: QualitySelector = QualitySelector.from(Quality.HIGHEST),
) : IntervalRecorderService.Settings(
maxDuration = maxDuration,
intervalDuration = intervalDuration
) {
val fileExtension
get() = "mp4"
fun getOutputOptions(video: VideoRecorderService): FileOutputOptions {
val fileName = "${video.counter}.$fileExtension"
val file = video.batchesFolder.getInternalFolder().resolve(fileName).apply {
createNewFile() createNewFile()
} }
@ -264,16 +249,7 @@ class VideoRecorderService :
} }
companion object { companion object {
fun from(appSettings: AppSettings) = Settings( const val CAMERA_CLOSE_TIMEOUT = 20000L
maxDuration = appSettings.maxDuration,
intervalDuration = appSettings.intervalDuration,
folder = appSettings.saveFolder,
targetVideoBitRate = appSettings.videoRecorderSettings.targetedVideoBitRate,
targetFrameRate = appSettings.videoRecorderSettings.targetFrameRate,
quality = appSettings.videoRecorderSettings.getQualitySelector()
?: QualitySelector.from(Quality.HIGHEST),
)
}
} }
class CameraControl( class CameraControl(

View File

@ -31,9 +31,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
typealias RecorderModel = BaseRecorderModel< typealias RecorderModel = BaseRecorderModel<
IntervalRecorderService.Settings,
RecordingInformation, RecordingInformation,
IntervalRecorderService<IntervalRecorderService.Settings, RecordingInformation>, IntervalRecorderService<RecordingInformation>,
BatchesFolder? BatchesFolder?
> >

View File

@ -56,7 +56,7 @@ fun AudioRecordingStatus(
recordingTime = audioRecorder.recordingTime, recordingTime = audioRecorder.recordingTime,
progress = audioRecorder.progress, progress = audioRecorder.progress,
recordingStart = audioRecorder.recordingStart, recordingStart = audioRecorder.recordingStart,
maxDuration = audioRecorder.settings.maxDuration, maxDuration = audioRecorder.settings!!.maxDuration,
) )
MicrophoneStatus(audioRecorder) MicrophoneStatus(audioRecorder)

View File

@ -110,7 +110,7 @@ fun VideoRecordingStatus(
recordingTime = videoRecorder.recordingTime, recordingTime = videoRecorder.recordingTime,
progress = videoRecorder.progress, progress = videoRecorder.progress,
recordingStart = videoRecorder.recordingStart, recordingStart = videoRecorder.recordingStart,
maxDuration = videoRecorder.settings.maxDuration, maxDuration = videoRecorder.settings!!.maxDuration,
) )
} }

View File

@ -13,7 +13,7 @@ import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.ui.utils.MicrophoneInfo import app.myzel394.alibi.ui.utils.MicrophoneInfo
class AudioRecorderModel : class AudioRecorderModel :
BaseRecorderModel<AudioRecorderService.Settings, RecordingInformation, AudioRecorderService, AudioBatchesFolder?>() { BaseRecorderModel<RecordingInformation, AudioRecorderService, AudioBatchesFolder?>() {
override var batchesFolder: AudioBatchesFolder? = null override var batchesFolder: AudioBatchesFolder? = null
override val intentClass = AudioRecorderService::class.java override val intentClass = AudioRecorderService::class.java
@ -46,8 +46,6 @@ class AudioRecorderModel :
amplitudes = amps amplitudes = amps
onAmplitudeChange() onAmplitudeChange()
} }
service.settings =
AudioRecorderService.Settings.from(settings)
service.clearAllRecordings() service.clearAllRecordings()
service.startRecording() service.startRecording()

View File

@ -19,7 +19,7 @@ import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService import app.myzel394.alibi.services.RecorderService
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : IntervalRecorderService<S, I>, B : BatchesFolder?> : abstract class BaseRecorderModel<I, T : IntervalRecorderService<I>, B : BatchesFolder?> :
ViewModel() { ViewModel() {
protected abstract val intentClass: Class<T> protected abstract val intentClass: Class<T>
@ -51,7 +51,7 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
private var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null private var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null
lateinit var settings: AppSettings var settings: AppSettings? = null
protected set protected set
protected abstract fun onServiceConnected(service: T) protected abstract fun onServiceConnected(service: T)
@ -77,6 +77,14 @@ abstract class BaseRecorderModel<S : IntervalRecorderService.Settings, I, T : In
batchesFolder = recorder.batchesFolder as B batchesFolder = recorder.batchesFolder as B
} }
if (settings != null) {
// If `settings` is set, it means we started the recording, so it should be
// properly set on the service
recorder.settings = settings!!
} else {
settings = recorder.settings
}
// Rest should be initialized from the child class // Rest should be initialized from the child class
onServiceConnected(recorder) onServiceConnected(recorder)
} }

View File

@ -17,7 +17,7 @@ import app.myzel394.alibi.ui.utils.CameraInfo
import app.myzel394.alibi.ui.utils.PermissionHelper import app.myzel394.alibi.ui.utils.PermissionHelper
class VideoRecorderModel : class VideoRecorderModel :
BaseRecorderModel<VideoRecorderService.Settings, RecordingInformation, VideoRecorderService, VideoBatchesFolder?>() { BaseRecorderModel<RecordingInformation, VideoRecorderService, VideoBatchesFolder?>() {
override var batchesFolder: VideoBatchesFolder? = null override var batchesFolder: VideoBatchesFolder? = null
override val intentClass = VideoRecorderService::class.java override val intentClass = VideoRecorderService::class.java
@ -39,8 +39,6 @@ class VideoRecorderModel :
} }
override fun onServiceConnected(service: VideoRecorderService) { override fun onServiceConnected(service: VideoRecorderService) {
service.settings = VideoRecorderService.Settings.from(settings)
service.clearAllRecordings() service.clearAllRecordings()
service.startRecording() service.startRecording()