feat: Add POC for CameraHandler

This commit is contained in:
Myzel394 2023-08-16 17:50:51 +02:00
parent 9997e0a3ec
commit f22f2c3802
No known key found for this signature in database
GPG Key ID: 50098FCA22080F0F
4 changed files with 270 additions and 91 deletions

View File

@ -95,7 +95,7 @@ dependencies {
implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.material:material-icons-extended:1.4.3" implementation "androidx.compose.material:material-icons-extended:1.5.0"
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.lifecycle:lifecycle-service:2.6.1' implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
@ -106,7 +106,7 @@ dependencies {
debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest' debugImplementation 'androidx.compose.ui:ui-test-manifest'
implementation "androidx.navigation:navigation-compose:2.7.0-rc01" implementation "androidx.navigation:navigation-compose:2.7.0"
implementation 'com.google.dagger:hilt-android:2.46.1' implementation 'com.google.dagger:hilt-android:2.46.1'
annotationProcessor 'com.google.dagger:hilt-compiler:2.46.1' annotationProcessor 'com.google.dagger:hilt-compiler:2.46.1'

View File

@ -0,0 +1,170 @@
package app.myzel394.alibi
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraDevice.StateCallback.ERROR_CAMERA_DEVICE
import android.hardware.camera2.CameraDevice.StateCallback.ERROR_CAMERA_SERVICE
import android.hardware.camera2.CameraManager
import android.hardware.camera2.params.OutputConfiguration
import android.hardware.camera2.params.SessionConfiguration
import android.media.Image
import android.media.ImageReader
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.core.content.ContentProviderCompat.requireContext
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.Executor
import java.util.logging.Logger
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class CameraHandler(
private val manager: CameraManager,
private val device: CameraDevice,
private val handler: Handler,
private val thread: HandlerThread,
) {
private lateinit var imageReader: ImageReader
@RequiresApi(Build.VERSION_CODES.P)
private suspend fun createCaptureSession(
outputs: List<Surface>,
): CameraCaptureSession = suspendCancellableCoroutine { cont ->
// I really don't want to use a deprecated method, but there is
// absolutely no documentation available for the new method.
device.createCaptureSession(
outputs,
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
cont.resume(session)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
cont.resumeWithException(RuntimeException("Failed to configure session"))
}
},
handler,
)
}
@RequiresApi(Build.VERSION_CODES.P)
public suspend fun takePhoto(
outputFile: File,
) {
Log.d("Alibi", "Taking photo")
Log.d("Alibi", "Creating Camera Characteristics")
val characteristics = manager.getCameraCharacteristics(CameraCharacteristics.LENS_FACING_BACK.toString())
Log.d("Alibi", "Creating size")
val size = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }!!
Log.d("Alibi", "Creating image reader")
imageReader = ImageReader.newInstance(
size.width,
size.height,
ImageFormat.JPEG,
1,
)
imageReader.setOnImageAvailableListener({ reader ->
val image = reader.acquireLatestImage()
// Save image to file
outputFile.outputStream().use { stream ->
image.planes[0].buffer.apply {
val bytes = ByteArray(remaining())
get(bytes)
stream.write(bytes)
}
}
println("Image saved to ${outputFile.absolutePath}")
image.close()
}, handler)
Log.d("Alibi", "Creating capture session")
val session = createCaptureSession(listOf(imageReader.surface))
Log.d("Alibi", "Creating capture request")
val captureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureRequest.addTarget(imageReader.surface)
Log.d("Alibi", "Capturing")
session.capture(captureRequest.build(), null, handler)
Log.d("Alibi", "Success!")
}
companion object {
const val IMAGE_BUFFER_SIZE = 3
fun getCameraManager(
context: Context,
): CameraManager = getSystemService(context, CameraManager::class.java)!!
fun createThread(): HandlerThread = HandlerThread("CameraHandler").apply { start() }
fun createHandler(thread: HandlerThread): Handler = Handler(thread.looper)
@SuppressLint("MissingPermission")
suspend fun openCamera(
manager: CameraManager,
cameraId: String,
thread: HandlerThread,
): CameraHandler = suspendCancellableCoroutine { cont ->
val handler = createHandler(thread)
manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
cont.resume(
CameraHandler(
manager,
device,
handler,
thread,
)
)
}
override fun onDisconnected(device: CameraDevice) {
Log.w("Alibi", "Camera $cameraId has been disconnected")
}
override fun onError(device: CameraDevice, error: Int) {
val msg = when (error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
Log.e("Alibi", exc.message, exc)
if (cont.isActive) cont.resumeWithException(exc)
}
}, handler)
}
suspend fun openCamera(
context: Context,
): CameraHandler {
val manager = getCameraManager(context)
val cameraId = manager.cameraIdList.first()
val thread = createThread()
return openCamera(manager, cameraId, thread)
}
}
}

View File

@ -20,7 +20,6 @@ import app.myzel394.alibi.services.RecorderService
import app.myzel394.alibi.services.VideoRecorderService import app.myzel394.alibi.services.VideoRecorderService
@ExperimentalVideo class VideoRecorderModel: ViewModel() { @ExperimentalVideo class VideoRecorderModel: ViewModel() {
/*
var recorderState by mutableStateOf(RecorderState.IDLE) var recorderState by mutableStateOf(RecorderState.IDLE)
private set private set
var recordingTime by mutableStateOf<Long?>(null) var recordingTime by mutableStateOf<Long?>(null)
@ -37,7 +36,7 @@ import app.myzel394.alibi.services.VideoRecorderService
get() = recorderState === RecorderState.PAUSED get() = recorderState === RecorderState.PAUSED
val progress: Float val progress: Float
get() = (recordingTime!! / recorderService!!.settings!!.maxDuration).toFloat() get() = 0f
private var intent: Intent? = null private var intent: Intent? = null
var recorderService: VideoRecorderService? = null var recorderService: VideoRecorderService? = null
@ -51,28 +50,6 @@ import app.myzel394.alibi.services.VideoRecorderService
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
recorderService = ((service as RecorderService.RecorderBinder).getService() as VideoRecorderService).also { recorder ->
recorder.onStateChange = { state ->
recorderState = state
}
recorder.onRecordingTimeChange = { time ->
recordingTime = time
}
recorder.onAmplitudeChange = { amps ->
amplitudes = amps
onAmplitudeChange()
}
recorder.onError = {
recorderService!!.createLastRecording()
onError()
}
recorderState = recorder.state
recordingTime = recorder.recordingTime
amplitudes = recorder.amplitudes
recorder.startRecording()
}
} }
override fun onServiceDisconnected(arg0: ComponentName) { override fun onServiceDisconnected(arg0: ComponentName) {
@ -99,7 +76,6 @@ import app.myzel394.alibi.services.VideoRecorderService
fun stopRecording(context: Context, saveAsLastRecording: Boolean = true) { fun stopRecording(context: Context, saveAsLastRecording: Boolean = true) {
if (saveAsLastRecording) { if (saveAsLastRecording) {
lastRecording = recorderService!!.createLastRecording()
} }
runCatching { runCatching {
@ -111,15 +87,12 @@ import app.myzel394.alibi.services.VideoRecorderService
} }
fun pauseRecording() { fun pauseRecording() {
recorderService!!.changeState(RecorderState.PAUSED)
} }
fun resumeRecording() { fun resumeRecording() {
recorderService!!.changeState(RecorderState.RECORDING)
} }
fun setMaxAmplitudesAmount(amount: Int) { fun setMaxAmplitudesAmount(amount: Int) {
recorderService?.amplitudesAmount = amount
} }
@Composable @Composable
@ -130,6 +103,4 @@ import app.myzel394.alibi.services.VideoRecorderService
} }
} }
} }
*/
} }

View File

@ -1,13 +1,18 @@
package app.myzel394.alibi.ui.screens package app.myzel394.alibi.ui.screens
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.ImageFormat import android.graphics.ImageFormat
import android.hardware.camera2.CameraCaptureSession import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.params.SessionConfiguration import android.hardware.camera2.params.SessionConfiguration
import android.media.Image import android.media.Image
import android.media.ImageReader import android.media.ImageReader
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture
@ -21,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -37,7 +43,9 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.navigation.NavController import androidx.navigation.NavController
import app.myzel394.alibi.CameraHandler
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.AudioRecordingStatus import app.myzel394.alibi.ui.components.RecorderScreen.molecules.AudioRecordingStatus
import app.myzel394.alibi.ui.components.RecorderScreen.molecules.StartRecording import app.myzel394.alibi.ui.components.RecorderScreen.molecules.StartRecording
@ -50,9 +58,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import androidx.compose.runtime.*
import java.io.File import java.io.File
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission", "NewApi")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RecorderScreen( fun RecorderScreen(
@ -64,63 +73,6 @@ fun RecorderScreen(
val lifecycle = LocalLifecycleOwner.current val lifecycle = LocalLifecycleOwner.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val cameraController = remember {LifecycleCameraController(context)}
LaunchedEffect(Unit) {
scope.launch {
// Take a picture
val manager = ContextCompat.getSystemService(context, android.hardware.camera2.CameraManager::class.java)!!
val cameraId = manager.cameraIdList.first()
manager.openCamera(
cameraId,
object: CameraDevice.StateCallback() {
override fun onDisconnected(p0: CameraDevice) {
}
override fun onError(p0: CameraDevice, p1: Int) {
}
override fun onOpened(p0: CameraDevice) {
val camDevice = p0
val cameraCharacteristics = manager.getCameraCharacteristics(cameraId)
val previewSize = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.getOutputSizes(
ImageFormat.JPEG).maxByOrNull { it.height * it.width }!!
val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1)
imageReader.setOnImageAvailableListener(
{ reader ->
val image: Image = reader.acquireLatestImage()
},
null
)
// Create capture
val captureBuilder = camDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureBuilder.addTarget(imageReader.surface)
// Create session
camDevice.createCaptureSession(
listOf(imageReader.surface),
object: CameraCaptureSession.StateCallback() {
override fun onConfigureFailed(p0: CameraCaptureSession) {
}
override fun onConfigured(p0: CameraCaptureSession) {
val session = p0
session.capture(captureBuilder.build(), object: CameraCaptureSession.CaptureCallback() {}, null)
}
},
null
)
}
},
null
)
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -141,6 +93,92 @@ fun RecorderScreen(
} }
) )
}, },
) {padding -> ) { padding ->
val scope = rememberCoroutineScope()
// Wait for `CameraHadler.openCamera`
var camera by remember { mutableStateOf<CameraHandler?>(null) }
Button(
modifier = Modifier
.padding(padding),
onClick = {
scope.launch {
if (camera == null) {
camera = CameraHandler.Companion.openCamera(context)
}
camera!!.takePhoto(
File(
context.externalCacheDir!!.absolutePath,
"test.jpg"
)
)
}
}) {
Text("Take photo")
}
/*
LaunchedEffect(Unit) {
val cameraManager = getSystemService(context, CameraManager::class.java)!!
val backgroundThread = HandlerThread("CameraVideo").apply {
start()
}
val backgroundHandler = Handler(backgroundThread.looper)
val characteristics = cameraManager.getCameraCharacteristics(CameraCharacteristics.LENS_FACING_BACK.toString())
val cameraStateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
val captureStateCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e("Camera", "Failed to configure camera")
}
override fun onConfigured(session: CameraCaptureSession) {
val onImageAvailableListener = object: ImageReader.OnImageAvailableListener{
override fun onImageAvailable(reader: ImageReader) {
val image: Image = reader.acquireLatestImage()
}
}
val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
session.capture(captureRequest.build(), null, backgroundHandler)
}
}
val previewSize = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }!!
val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1)
camera.createCaptureSession(
listOf(imageReader.surface),
captureStateCallback,
backgroundHandler
)
}
override fun onDisconnected(cameraDevice: CameraDevice) {
}
override fun onError(cameraDevice: CameraDevice, error: Int) {
val errorMsg = when(error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
}
}
cameraManager.openCamera(
CameraCharacteristics.LENS_FACING_BACK.toString(),
cameraStateCallback,
backgroundHandler
)
}
*/
} }
} }