mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-18 23:05:26 +02:00
feat: Slowly creating workable camera support
This commit is contained in:
parent
d21580b0cb
commit
f033550f8f
@ -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 -->
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user