From b19f5d8be9df52df32428ed4a3df8f7a468b1bdb Mon Sep 17 00:00:00 2001 From: RigoLigo Date: Wed, 11 Oct 2023 18:01:35 +0800 Subject: [PATCH] Add logical camera support --- .../samsung/android/scan3d/serv/CamEngine.kt | 59 ++++++++++----- .../samsung/android/scan3d/util/Selector.kt | 74 +++++++++++-------- 2 files changed, 85 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt b/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt index 39a7326..555760e 100644 --- a/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt +++ b/app/src/main/java/com/samsung/android/scan3d/serv/CamEngine.kt @@ -10,6 +10,8 @@ import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraManager import android.hardware.camera2.CaptureRequest import android.hardware.camera2.TotalCaptureResult +import android.hardware.camera2.params.OutputConfiguration +import android.hardware.camera2.params.SessionConfiguration import android.media.ImageReader import android.media.MediaCodec import android.media.MediaCodecInfo @@ -32,6 +34,7 @@ import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger + class CamEngine(val context: Context) { var http: HttpService? = null @@ -86,7 +89,7 @@ class CamEngine(val context: Context) { var viewState: CameraFragment.Companion.ViewState = CameraFragment.Companion.ViewState( true, stream = false, - cameraId = "0", + cameraId = cameraList.first().cameraId, quality = 80, resolutionIndex = null ) @@ -133,14 +136,16 @@ class CamEngine(val context: Context) { private suspend fun openCamera( manager: CameraManager, cameraId: String, + logicalCameraId: String?, handler: Handler? = null ): CameraDevice = suspendCancellableCoroutine { cont -> - manager.openCamera(cameraId, object : CameraDevice.StateCallback() { + + val idToOpen = logicalCameraId ?: cameraId; + manager.openCamera(idToOpen, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) = cont.resume(device) override fun onDisconnected(device: CameraDevice) { Log.w("CamEngine", "Camera $cameraId has been disconnected") - } override fun onError(device: CameraDevice, error: Int) { @@ -165,22 +170,39 @@ class CamEngine(val context: Context) { */ private suspend fun createCaptureSession( device: CameraDevice, + physicalCamId: String?, targets: List, handler: Handler? = null ): CameraCaptureSession = suspendCoroutine { cont -> + val outputConfigs = mutableListOf(); + targets.forEach { + outputConfigs.add(OutputConfiguration(it).apply { + // If physical camera id is not null, it's a logical cam, you should set it + if (physicalCamId != null) { + setPhysicalCameraId(physicalCamId) + } + }) + } // Create a capture session using the predefined targets; this also involves defining the // session state callback to be notified of when the session is ready - device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() { - - override fun onConfigured(session: CameraCaptureSession) = cont.resume(session) - - override fun onConfigureFailed(session: CameraCaptureSession) { - val exc = RuntimeException("Camera ${device.id} session configuration failed") - Log.e("CamEngine", exc.message, exc) - cont.resumeWithException(exc) + val sessionConfiguration = SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + object : CameraCaptureSession.StateCallback() { + + override fun onConfigured(session: CameraCaptureSession) = cont.resume(session) + + override fun onConfigureFailed(session: CameraCaptureSession) { + val exc = RuntimeException("Camera ${device.id} session configuration failed") + Log.e("CamEngine", exc.message, exc) + cont.resumeWithException(exc) + } } - }, handler) + ) + + device.createCaptureSession(sessionConfiguration) } suspend fun initializeCamera() { @@ -212,18 +234,21 @@ class CamEngine(val context: Context) { resH = sizes[viewState.resolutionIndex!!].height - - camera = openCamera(cameraManager, viewState.cameraId, cameraHandler) + val sensor = cameraList.find { it.cameraId == viewState.cameraId }!! + camera = openCamera(cameraManager, sensor.cameraId, sensor.logicalCameraId, cameraHandler) imageReader = ImageReader.newInstance( resW, resH, camOutPutFormat, 4 ) var targets = listOf(imageReader.surface) if (showLiveSurface) { - - targets = targets.plus(previewSurface!!) } - session = createCaptureSession(camera, targets, cameraHandler) + session = createCaptureSession( + camera, + if (sensor.logicalCameraId == null) null else sensor.cameraId, + targets, + cameraHandler + ) val captureRequest = camera.createCaptureRequest( CameraDevice.TEMPLATE_RECORD //TEMPLATE_PREVIEW ) diff --git a/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt b/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt index 35e3b7f..fa85751 100644 --- a/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt +++ b/app/src/main/java/com/samsung/android/scan3d/util/Selector.kt @@ -15,7 +15,12 @@ import kotlin.math.roundToInt object Selector { /** Helper class used as a data holder for each selectable camera format item */ @Parcelize - data class SensorDesc(val title: String, val cameraId: String, val format: Int) : Parcelable + data class SensorDesc( + val title: String, + val cameraId: String, + val logicalCameraId: String?, + val format: Int + ) : Parcelable /** Helper function used to convert a lens orientation enum into a human-readable string */ private fun lensOrientationString(value: Int) = when (value) { @@ -66,38 +71,33 @@ object Selector { // Get list of all compatible cameras val cameraIds = mutableListOf() - for (i in 0..100) { - - val istr = i.toString() - if (!cameraIds.contains(istr)) { - cameraIds.add(istr) - } + cameraManager.cameraIdList.forEach { it -> + cameraIds.add(it.toString()) } - val cameraIds2 = mutableListOf() - cameraIds.filter { + val cameraIds2 = mutableListOf() + val openableCameraIds = mutableListOf() + cameraIds.forEach { id -> try { - val characteristics = cameraManager.getCameraCharacteristics(it) + val characteristics = cameraManager.getCameraCharacteristics(id) val capabilities = characteristics.get( CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES - ) + ) ?: return@forEach - if (capabilities == null) { - false - } else if (capabilities.contains( + if (capabilities.contains( CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA ) ) { - false - } else if (capabilities.contains( - CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE - ) - ) { - true + // We got evil logical camera here, split it up into physical ones + characteristics.physicalCameraIds.forEach { physId -> + cameraIds2.add(SensorDesc("", physId, id, 0)) + } } else { - false + cameraIds2.add(SensorDesc("", id, null, 0)) + openableCameraIds.add(id) } + } catch (_: Exception) { } catch (e: Exception) { @@ -105,12 +105,23 @@ object Selector { } }.forEach { cameraIds2.add(it) } + // There can be physical cameras that you can access both from a logical camera and as a + // physical camera. (For example, on Galaxy S23 Ultra, the ultra-wide camera) + // We remove the duplicates here. + // We enumerate all directly openable camera IDs, then search in cameraIds2 for a logical + // camera entry that contains it, then delete it + val removeList = mutableListOf() + openableCameraIds.forEach { id -> + cameraIds2.removeAll { + it.logicalCameraId != null && it.cameraId == id + } + } // Iterate over the list of cameras and return all the compatible ones - cameraIds2.forEach { id -> + cameraIds2.forEach { desc -> - Log.i("SELECTOR", "id: " + id) - val characteristics = cameraManager.getCameraCharacteristics(id) + Log.i("SELECTOR", "Camera ${desc.cameraId} @ LogicalCam ${desc.logicalCameraId}") + val characteristics = cameraManager.getCameraCharacteristics(desc.cameraId) val orientation = lensOrientationString( characteristics.get(CameraCharacteristics.LENS_FACING)!! ) @@ -122,7 +133,6 @@ object Selector { capabilities.forEach { Log.i("CAP", "" + getCapStringAtIndex(it)) } - val outputFormats = characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP )!!.outputFormats @@ -148,17 +158,19 @@ object Selector { // All cameras *must* support JPEG output so we don't need to check characteristics - val title= "vfov:$vfov $foc $ape $orientation" - if(!availableCameras.any {it-> it.title==title } ){ + val camId = if (desc.logicalCameraId == null) + "${desc.cameraId}" + else + "${desc.cameraId}@${desc.logicalCameraId}" + + val title = "$camId vfov:$vfov $foc $ape $orientation" + if (!availableCameras.any { it -> it.title == title }) { availableCameras.add( SensorDesc( - title, id, ImageFormat.JPEG + title, desc.cameraId, desc.logicalCameraId, ImageFormat.JPEG ) ) } - - - } return availableCameras