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:name=".services.AudioRecorderService"
|
||||||
android:foregroundServiceType="microphone" />
|
android:foregroundServiceType="microphone" />
|
||||||
<service
|
<service
|
||||||
android:name=".services.OldVideoService"
|
android:name=".services.VideoService"
|
||||||
android:foregroundServiceType="camera|microphone" />
|
android:foregroundServiceType="camera|microphone" />
|
||||||
|
|
||||||
<!-- Change locale for Android <= 12 -->
|
<!-- Change locale for Android <= 12 -->
|
||||||
|
@ -289,7 +289,7 @@ class AudioRecorderService :
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(audioRecorderSettings: AudioRecorderSettings): IntervalRecorderService.Settings {
|
fun from(audioRecorderSettings: AudioRecorderSettings): Settings {
|
||||||
return Settings(
|
return Settings(
|
||||||
intervalDuration = audioRecorderSettings.intervalDuration,
|
intervalDuration = audioRecorderSettings.intervalDuration,
|
||||||
bitRate = audioRecorderSettings.bitRate,
|
bitRate = audioRecorderSettings.bitRate,
|
||||||
|
@ -43,9 +43,7 @@ abstract class IntervalRecorderService<S : IntervalRecorderService.Settings, I>
|
|||||||
private fun createTimer() {
|
private fun createTimer() {
|
||||||
cycleTimer = Executors.newSingleThreadScheduledExecutor().also {
|
cycleTimer = Executors.newSingleThreadScheduledExecutor().also {
|
||||||
it.scheduleAtFixedRate(
|
it.scheduleAtFixedRate(
|
||||||
{
|
::startNewCycle,
|
||||||
startNewCycle()
|
|
||||||
},
|
|
||||||
0,
|
0,
|
||||||
settings.intervalDuration,
|
settings.intervalDuration,
|
||||||
TimeUnit.MILLISECONDS
|
TimeUnit.MILLISECONDS
|
||||||
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
@ -12,21 +13,177 @@ import androidx.camera.video.Quality
|
|||||||
import androidx.camera.video.QualitySelector
|
import androidx.camera.video.QualitySelector
|
||||||
import androidx.camera.video.Recorder
|
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.withOutput
|
import androidx.camera.video.VideoCapture.withOutput
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import app.myzel394.alibi.NotificationHelper
|
import app.myzel394.alibi.NotificationHelper
|
||||||
|
import app.myzel394.alibi.db.RecordingInformation
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
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() {
|
class OldVideoService : LifecycleService() {
|
||||||
|
@ -92,7 +92,7 @@ class AudioRecorderModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
recorder.batchesFolder = batchesFolder ?: recorder.batchesFolder
|
recorder.batchesFolder = batchesFolder ?: recorder.batchesFolder
|
||||||
recorder.settings =
|
recorder.settings =
|
||||||
IntervalRecorderService.Settings.from(settings.audioRecorderSettings)
|
AudioRecorderService.Settings.from(settings.audioRecorderSettings)
|
||||||
|
|
||||||
recorder.clearAllRecordings()
|
recorder.clearAllRecordings()
|
||||||
}.also {
|
}.also {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package app.myzel394.alibi.ui.screens
|
package app.myzel394.alibi.ui.screens
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
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
|
@Composable
|
||||||
fun POCVideo() {
|
fun POCVideo() {
|
||||||
@ -23,8 +28,24 @@ fun POCVideo() {
|
|||||||
.padding(64.dp)
|
.padding(64.dp)
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
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)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||||
}) {
|
}) {
|
||||||
Text("Start")
|
Text("Start")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user