feat: Slowly creating workable camera support

This commit is contained in:
Myzel394 2023-11-26 19:10:15 +01:00
parent d21580b0cb
commit f033550f8f
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
6 changed files with 185 additions and 9 deletions

View File

@ -52,7 +52,7 @@
android:name=".services.AudioRecorderService"
android:foregroundServiceType="microphone" />
<service
android:name=".services.OldVideoService"
android:name=".services.VideoService"
android:foregroundServiceType="camera|microphone" />
<!-- Change locale for Android <= 12 -->

View File

@ -289,7 +289,7 @@ class AudioRecorderService :
}
companion object {
fun from(audioRecorderSettings: AudioRecorderSettings): IntervalRecorderService.Settings {
fun from(audioRecorderSettings: AudioRecorderSettings): Settings {
return Settings(
intervalDuration = audioRecorderSettings.intervalDuration,
bitRate = audioRecorderSettings.bitRate,

View File

@ -43,9 +43,7 @@ abstract class IntervalRecorderService<S : IntervalRecorderService.Settings, I>
private fun createTimer() {
cycleTimer = Executors.newSingleThreadScheduledExecutor().also {
it.scheduleAtFixedRate(
{
startNewCycle()
},
::startNewCycle,
0,
settings.intervalDuration,
TimeUnit.MILLISECONDS

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.camera.core.CameraSelector
import androidx.camera.lifecycle.ProcessCameraProvider
@ -12,21 +13,177 @@ import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoCapture.withOutput
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleService
import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.db.RecordingInformation
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
class VideoService : IntervalRecorderService() {
class VideoService : IntervalRecorderService<VideoService.Settings, RecordingInformation>() {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private var cameraProvider: ProcessCameraProvider? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var activeRecording: Recording? = null
// Used to listen and check if the camera is available
private var _cameraAvailableListener = CompletableDeferred<Boolean>()
// Runs a function in the main thread
private fun runInMain(callback: () -> Unit) {
val mainHandler = ContextCompat.getMainExecutor(this)
mainHandler.execute(callback)
}
// Open the camera.
// Used to open it for a longer time, shouldn't be called when pausing / resuming.
// This should only be called when starting a recording.
private suspend fun openCamera() {
cameraProvider = withContext(Dispatchers.IO) {
ProcessCameraProvider.getInstance(this@VideoService).get()
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = withOutput(recorder)
runInMain {
cameraProvider!!.bindToLifecycle(
this,
settings.camera,
videoCapture
)
_cameraAvailableListener.complete(true)
}
}
// Close the camera
// Used to close it finally, shouldn't be called when pausing / resuming.
// This should only be called after recording has finished.
private fun closeCamera() {
clearOldVideoRecording()
runCatching {
cameraProvider?.unbindAll()
}
cameraProvider = null
videoCapture = null
}
override fun start() {
super.start()
scope.launch {
openCamera()
}
}
override fun stop() {
super.stop()
closeCamera()
}
private fun clearOldVideoRecording() {
runCatching {
activeRecording?.stop()
}
}
@SuppressLint("MissingPermission")
private fun prepareVideoRecording() =
videoCapture!!.output
.prepareRecording(this, settings.getOutputOptions(this))
.withAudioEnabled()
@SuppressLint("MissingPermission")
override fun startNewCycle() {
super.startNewCycle()
fun action() {
println("=======================")
activeRecording?.stop()
val newRecording = prepareVideoRecording()
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this), {})
}
if (_cameraAvailableListener.isCompleted) {
action()
} else {
// Race condition of `startNewCycle` being called before `invpkeOnCompletion`
// has been called can be ignored, as the camera usually opens within 5 seconds
// and the interval can't be set shorter than 10 seconds.
_cameraAvailableListener.invokeOnCompletion {
action()
}
}
}
override fun getRecordingInformation(): RecordingInformation = RecordingInformation(
folderPath = batchesFolder.exportFolderForSettings(),
recordingStart = recordingStart,
maxDuration = settings.maxDuration,
fileExtension = settings.fileExtension,
intervalDuration = settings.intervalDuration,
)
data class Settings(
override val maxDuration: Long,
override val intervalDuration: Long,
val folder: String? = null,
val camera: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
) : IntervalRecorderService.Settings(
maxDuration = maxDuration,
intervalDuration = intervalDuration
) {
val fileExtension
get() = "mp4"
fun getOutputOptions(video: VideoService): MediaStoreOutputOptions {
val contentValues = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, "${video.counter}.$fileExtension")
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"DCIM/Recordings"
)
}
// TODO: Find a way to make this work with the internal storage
return MediaStoreOutputOptions.Builder(
video.contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
)
.setContentValues(contentValues)
.build()
}
companion object {
fun from() = Settings(
maxDuration = 60_000,
intervalDuration = 10_000,
)
}
}
}
class OldVideoService : LifecycleService() {

View File

@ -92,7 +92,7 @@ class AudioRecorderModel : ViewModel() {
}
recorder.batchesFolder = batchesFolder ?: recorder.batchesFolder
recorder.settings =
IntervalRecorderService.Settings.from(settings.audioRecorderSettings)
AudioRecorderService.Settings.from(settings.audioRecorderSettings)
recorder.clearAllRecordings()
}.also {

View File

@ -1,6 +1,9 @@
package app.myzel394.alibi.ui.screens
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.IBinder
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@ -11,7 +14,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import app.myzel394.alibi.services.OldVideoService
import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.RecorderService
import app.myzel394.alibi.services.VideoService
@Composable
fun POCVideo() {
@ -23,8 +28,24 @@ fun POCVideo() {
.padding(64.dp)
) {
Button(onClick = {
val intent = Intent(context, OldVideoService::class.java)
val connection = object : android.content.ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder =
((service as RecorderService.RecorderBinder).getService() as VideoService).also { recorder ->
recorder.settings = VideoService.Settings.from()
recorder.startRecording()
}
}
override fun onServiceDisconnected(name: ComponentName?) {
}
}
val intent = Intent(context, VideoService::class.java).apply {
action = "init"
}
ContextCompat.startForegroundService(context, intent)
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}) {
Text("Start")
}