Created
February 14, 2025 18:32
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context | |
import android.hardware.camera2.CameraManager | |
import android.hardware.camera2.CameraMetadata | |
import android.os.Build | |
import android.util.Log | |
import android.view.View | |
import androidx.annotation.OptIn | |
import androidx.annotation.RequiresApi | |
import androidx.camera.camera2.interop.Camera2CameraInfo | |
import androidx.camera.camera2.interop.Camera2Interop | |
import androidx.camera.camera2.interop.ExperimentalCamera2Interop | |
import androidx.camera.core.CameraSelector | |
import androidx.camera.core.Preview | |
import androidx.camera.lifecycle.ProcessCameraProvider | |
import androidx.camera.view.PreviewView | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.ColorMatrix | |
import androidx.compose.ui.graphics.ColorMatrixColorFilter | |
import androidx.compose.ui.graphics.Paint | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.viewinterop.AndroidView | |
import androidx.lifecycle.compose.LocalLifecycleOwner | |
@RequiresApi(Build.VERSION_CODES.TIRAMISU) | |
@OptIn(ExperimentalCamera2Interop::class) | |
@Composable | |
fun CameraImageView( | |
modifier: Modifier = Modifier, | |
isGreyscale: Boolean = false, | |
cameraId: String? | |
) { | |
val context = LocalContext.current | |
val lifecycleOwner = LocalLifecycleOwner.current | |
var showError by remember { mutableStateOf(false) } | |
val previewView = remember { | |
PreviewView(context).apply { | |
scaleType = PreviewView.ScaleType.FILL_CENTER | |
implementationMode = PreviewView.ImplementationMode.COMPATIBLE | |
} | |
} | |
AndroidView( | |
modifier = modifier, | |
factory = { previewView }, | |
update = { view -> | |
if (isGreyscale) applyGreyscale(view) else resetView(view) | |
} | |
) | |
LaunchedEffect(cameraId) { | |
if (cameraId.isNullOrEmpty()) return@LaunchedEffect | |
try { | |
val cameraProvider = ProcessCameraProvider.getInstance(context).get() | |
cameraProvider.unbindAll() | |
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager | |
// Find if this is a physical camera and get its logical parent | |
val (isPhysical, logicalParentId) = findLogicalParent(cameraManager, cameraId) | |
val selectorId = logicalParentId ?: cameraId | |
// Build preview with Camera2Interop if physical | |
val preview = Preview.Builder().apply { | |
if (isPhysical) { | |
val characteristics = cameraManager.getCameraCharacteristics(selectorId) | |
if (characteristics.physicalCameraIds.contains(cameraId)) { | |
Camera2Interop.Extender(this) | |
.setPhysicalCameraId(cameraId) | |
.setStreamUseCase(CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW.toLong()) | |
} else { | |
Log.e( | |
"CameraView", | |
"Requested physical camera ID $cameraId is not valid for logical camera $selectorId" | |
) | |
} | |
} | |
}.build().also { it.surfaceProvider = previewView.surfaceProvider } | |
// Use logical parent if available, otherwise original ID | |
val selector = CameraSelector.Builder() | |
.addCameraFilter { | |
cameraProvider.availableCameraInfos.filter { | |
Camera2CameraInfo.from(it).cameraId == selectorId | |
} | |
} | |
.build() | |
cameraProvider.bindToLifecycle( | |
lifecycleOwner, | |
selector, | |
preview | |
) | |
showError = false | |
} catch (e: Exception) { | |
Log.e("CameraView", "Binding failed: ${e.message}") | |
showError = true | |
} | |
} | |
if (showError) { | |
Box(modifier) { | |
Text("Camera unavailable", color = Color.Red) | |
} | |
} | |
} | |
private fun findLogicalParent( | |
cameraManager: CameraManager, | |
cameraId: String | |
): Pair<Boolean, String?> { | |
return try { | |
// Check if this camera is a physical sub-camera | |
val allCameraIds = cameraManager.cameraIdList | |
var logicalParent: String? = null | |
for (potentialLogicalId in allCameraIds) { | |
val chars = cameraManager.getCameraCharacteristics(potentialLogicalId) | |
if (chars.physicalCameraIds.contains(cameraId)) { | |
logicalParent = potentialLogicalId | |
break | |
} | |
} | |
// Return if physical and parent ID | |
(logicalParent != null) to logicalParent | |
} catch (e: Exception) { | |
false to null | |
} | |
} | |
private fun applyGreyscale(view: PreviewView) { | |
val paint = Paint().apply { | |
colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { setToSaturation(0f) }) | |
} | |
view.setLayerType(View.LAYER_TYPE_HARDWARE, paint.asFrameworkPaint()) | |
} | |
private fun resetView(view: PreviewView) { | |
view.setLayerType(View.LAYER_TYPE_NONE, null) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum class CameraFacing { | |
FRONT, BACK, UNKNOWN | |
} | |
data class CameraOption( | |
val cameraId: String, | |
val lensType: LensType, | |
val description: String, | |
val facing: CameraFacing | |
) | |
@Singleton | |
class CameraRepository @Inject constructor( | |
private val context: Context | |
) { | |
private fun createCameraOption( | |
cameraId: String, | |
characteristics: CameraCharacteristics | |
): CameraOption { | |
return CameraOption( | |
cameraId = cameraId, | |
lensType = determineLensType(characteristics), | |
description = "Camera $cameraId", | |
facing = getFacingDirection(characteristics) | |
) | |
} | |
fun getAllCameraOptions(): List<CameraOption> { | |
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager | |
return cameraManager.cameraIdList.flatMap { cameraId -> | |
try { | |
val characteristics = cameraManager.getCameraCharacteristics(cameraId) | |
val cameras = mutableListOf<CameraOption>() | |
// Add logical camera | |
cameras.add(createCameraOption(cameraId, characteristics)) | |
// Add physical cameras if available | |
if (isLogicalCamera(characteristics)) { | |
characteristics.physicalCameraIds.forEach { physicalId -> | |
try { | |
val physicalChars = cameraManager.getCameraCharacteristics(physicalId) | |
cameras.add(createCameraOption(physicalId, physicalChars)) | |
} catch (e: Exception) { | |
Log.e("CameraRepo", "Error processing physical camera $physicalId", e) | |
} | |
} | |
} | |
cameras | |
} catch (e: Exception) { | |
Log.e("CameraRepo", "Error processing camera $cameraId", e) | |
emptyList() | |
} | |
} | |
} | |
private fun isLogicalCamera(characteristics: CameraCharacteristics): Boolean { | |
val capabilities = characteristics.get( | |
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES | |
) ?: return false | |
return capabilities.contains( | |
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA | |
) | |
} | |
private fun determineLensType(characteristics: CameraCharacteristics): LensType { | |
val focalLengths = | |
characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) | |
val focalLength = focalLengths?.firstOrNull() ?: 0f | |
return when { | |
focalLength < 4.0f -> LensType.ULTRAWIDE | |
focalLength in 4.0f..6.0f -> LensType.WIDE | |
focalLength > 6.0f -> LensType.TELEPHOTO | |
else -> LensType.UNKNOWN | |
} | |
} | |
private fun getFacingDirection(characteristics: CameraCharacteristics): CameraFacing { | |
return when (characteristics.get(CameraCharacteristics.LENS_FACING)) { | |
CameraCharacteristics.LENS_FACING_FRONT -> CameraFacing.FRONT | |
CameraCharacteristics.LENS_FACING_BACK -> CameraFacing.BACK | |
else -> CameraFacing.UNKNOWN | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment