From da1438c98922fad661f0b3a47b40498c88f5c084 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:06:50 +0200 Subject: [PATCH] feat: Add first working non-stretched camera preview --- .../java/app/myzel394/alibi/CameraHandler.kt | 8 +- .../alibi/ui/screens/RecorderScreen.kt | 184 ++++++++---------- .../app/myzel394/alibi/ui/utils/camera.kt | 57 ++++++ 3 files changed, 148 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt diff --git a/app/src/main/java/app/myzel394/alibi/CameraHandler.kt b/app/src/main/java/app/myzel394/alibi/CameraHandler.kt index 7c9b392..df68c5d 100644 --- a/app/src/main/java/app/myzel394/alibi/CameraHandler.kt +++ b/app/src/main/java/app/myzel394/alibi/CameraHandler.kt @@ -8,6 +8,7 @@ import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraManager import android.media.ImageReader +import android.net.wifi.aware.Characteristics import android.os.Build import android.os.Handler import android.os.HandlerThread @@ -26,9 +27,13 @@ class CameraHandler( private val device: CameraDevice, private val handler: Handler, private val thread: HandlerThread, + private val lens: Int = CameraCharacteristics.LENS_FACING_BACK, ) { private lateinit var imageReader: ImageReader + val characteristics: CameraCharacteristics + get() = manager.getCameraCharacteristics(lens.toString()) + @RequiresApi(Build.VERSION_CODES.P) private suspend fun createCaptureSession( outputs: List, @@ -75,7 +80,7 @@ class CameraHandler( Log.d("Alibi", "Taking photo") Log.d("Alibi", "Creating Camera Characteristics") - val characteristics = manager.getCameraCharacteristics(CameraCharacteristics.LENS_FACING_BACK.toString()) + val characteristics = manager.getCameraCharacteristics(lens.toString()) Log.d("Alibi", "Creating size") val size = characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! @@ -148,7 +153,6 @@ class CameraHandler( } fun getPreviewSize(): Size { - val characteristics = manager.getCameraCharacteristics(CameraCharacteristics.LENS_FACING_BACK.toString()) return characteristics .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! .getOutputSizes(ImageFormat.JPEG) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt index d6d8a35..d450457 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/RecorderScreen.kt @@ -1,66 +1,25 @@ package app.myzel394.alibi.ui.screens 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.CameraManager -import android.hardware.camera2.params.SessionConfiguration -import android.media.Image -import android.media.ImageReader -import android.os.Handler -import android.os.HandlerThread -import android.util.Log +import android.util.Size import android.view.SurfaceHolder import android.view.SurfaceView import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import androidx.camera.core.ImageCapture -import androidx.camera.core.ImageCaptureException -import androidx.camera.core.ImageProxy -import androidx.camera.view.CameraController.VIDEO_CAPTURE -import androidx.camera.view.LifecycleCameraController -import androidx.camera.view.PreviewView -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat -import androidx.core.content.ContextCompat.getSystemService import androidx.navigation.NavController import app.myzel394.alibi.CameraHandler -import app.myzel394.alibi.R -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.VideoRecordingStatus -import app.myzel394.alibi.ui.enums.Screen import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.models.VideoRecorderModel -import com.ujizin.camposer.CameraPreview -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay +import app.myzel394.alibi.ui.utils.getOptimalPreviewSize import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import androidx.compose.runtime.* import java.io.File @SuppressLint("MissingPermission", "NewApi") @@ -72,74 +31,101 @@ fun RecorderScreen( videoRecorder: VideoRecorderModel, ) { val context = LocalContext.current - val lifecycle = LocalLifecycleOwner.current val scope = rememberCoroutineScope() var camera by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + camera = CameraHandler.openCamera(context) + } - AndroidView( - factory = {context -> - val surfaceView = SurfaceView(context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) + if (camera == null) { + return + } else { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + val surface = object : SurfaceView(context) { + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = resolveSize(suggestedMinimumWidth, widthMeasureSpec); + val height = resolveSize(suggestedMinimumHeight, heightMeasureSpec); + setMeasuredDimension(width, height); + + val supportedPreviewSizes = camera!! + .characteristics + .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! + .getOutputSizes(SurfaceHolder::class.java) + + if (supportedPreviewSizes != null) { + val previewSize = getOptimalPreviewSize(supportedPreviewSizes, width, height); + + val ratio = if (previewSize.height >= previewSize.width) + (previewSize.height / previewSize.width).toFloat() + else (previewSize.width / previewSize.height).toFloat() + + val optimalWidth = width + val optimalHeight = (width * ratio).toInt() + + // Make sure the camera preview uses the whole screen + + val metrics = context.resources.displayMetrics + layoutParams.width = metrics.widthPixels + layoutParams.height = metrics.heightPixels + + setMeasuredDimension(optimalWidth, optimalHeight) + + } + } + } + + val surfaceView = surface.apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + } + + surfaceView.holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + scope.launch { + camera!!.startPreview(holder.surface) + } + } + + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) { + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + scope.launch { + camera!!.stopPreview() + } + } + }) + + surfaceView } - surfaceView.holder.addCallback(object : SurfaceHolder.Callback { - override fun surfaceCreated(holder: SurfaceHolder) { - scope.launch { - camera = CameraHandler.openCamera(context) - - val previewSize = camera!!.getPreviewSize() - - holder.setFixedSize( - 1080, - 1920, - ) - - camera!!.startPreview(holder.surface) - } - } - - override fun surfaceChanged( - holder: SurfaceHolder, - format: Int, - width: Int, - height: Int - ) { - } - - override fun surfaceDestroyed(holder: SurfaceHolder) { - scope.launch { - camera!!.stopPreview() - } - } - }) - - surfaceView - } - - ) + ) Button( onClick = { - scope.launch { - if (camera == null) { - camera = CameraHandler.openCamera(context) - } - - camera!!.takePhoto( - File( - context.externalCacheDir!!.absolutePath, - "test.jpg" + scope.launch { + camera!!.takePhoto( + File( + context.externalCacheDir!!.absolutePath, + "test.jpg" + ) ) - ) - } - }) { + } + }) { Text("Take photo") } + } /* LaunchedEffect(Unit) { diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt new file mode 100644 index 0000000..3cb9ade --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/camera.kt @@ -0,0 +1,57 @@ +package app.myzel394.alibi.ui.utils + +import android.hardware.camera2.CameraCharacteristics +import android.util.Size +import kotlin.math.abs + +/** + * Computes rotation required to transform the camera sensor output orientation to the + * device's current orientation in degrees. + * + * @param characteristics The CameraCharacteristics to query for the sensor orientation. + * @param surfaceRotationDegrees The current device orientation as a Surface constant. + * @return Relative rotation of the camera sensor output. + */ +public fun computeRelativeRotation( + characteristics: CameraCharacteristics, + surfaceRotationDegrees: Int +): Int { + val sensorOrientationDegrees = + characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! + + // Reverse device orientation for back-facing cameras. + val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == + CameraCharacteristics.LENS_FACING_FRONT + ) 1 else -1 + + // Calculate desired orientation relative to camera orientation to make + // the image upright relative to the device orientation. + return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360 +} + +fun getOptimalPreviewSize(sizes: Array, w: Int, h: Int): Size { + val ASPECT_TOLERANCE = 0.1 + val targetRatio = h.toDouble() / w + var optimalSize: Size? = null + var minDiff = Double.MAX_VALUE + + for (size in sizes) { + val ratio = size.width / size.height + if (abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue + if (abs(size.height - h) < minDiff) { + optimalSize = size + minDiff = abs(size.height - h).toDouble() + } + } + + if (optimalSize == null) { + minDiff = Double.MAX_VALUE + for (size in sizes) { + if (abs(size.height - h) < minDiff) { + optimalSize = size + minDiff = abs(size.height - h).toDouble() + } + } + } + return optimalSize!! +} \ No newline at end of file