Kasem Mohamed
Committed by GitHub

Support for selecting cameras by their physical id (#668)

* Support selecting physical camera

This commit adds support for selecting a specific physical camera when multiple physical cameras are available under a single logical camera. This is particularly relevant for devices with multi-camera systems.

* Return early

* Fix typo

* Add comment

* Fix comment

* Make function private

* add changeset

---------

Co-authored-by: davidliu <davidliu@deviange.net>
---
"client-sdk-android": minor
---
CameraX: support for selecting cameras by their physical id
... ...
/*
* Copyright 2024 LiveKit, Inc.
* Copyright 2024-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -37,6 +37,7 @@ internal class CameraXCapturer(
cameraName: String?,
eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
private val useCases: Array<out UseCase> = emptyArray(),
var physicalCameraId: String? = null,
) : CameraCapturer(cameraName, eventsHandler, CameraXEnumerator(context, lifecycleOwner)) {
@FlowObservable
... ... @@ -93,6 +94,7 @@ internal class CameraXCapturer(
height,
framerate,
useCases,
physicalCameraId,
)
}
}
... ...
/*
* Copyright 2024 LiveKit, Inc.
* Copyright 2024-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -35,13 +35,14 @@ class CameraXEnumerator(
context: Context,
private val lifecycleOwner: LifecycleOwner,
private val useCases: Array<out UseCase> = emptyArray(),
var physicalCameraId: String? = null,
) : Camera2Enumerator(context) {
override fun createCapturer(
deviceName: String?,
eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
): CameraVideoCapturer {
return CameraXCapturer(context, lifecycleOwner, deviceName, eventsHandler, useCases)
return CameraXCapturer(context, lifecycleOwner, deviceName, eventsHandler, useCases, physicalCameraId)
}
companion object {
... ...
/*
* Copyright 2023-2024 LiveKit, Inc.
* Copyright 2023-2025 LiveKit, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
... ... @@ -18,6 +18,7 @@ package livekit.org.webrtc
import android.content.Context
import android.hardware.camera2.CameraManager
import android.os.Build
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.UseCase
import androidx.lifecycle.Lifecycle
... ... @@ -61,12 +62,21 @@ class CameraXHelper {
eventsHandler: CameraEventsDispatchHandler,
): VideoCapturer {
val enumerator = provideEnumerator(context)
val targetDeviceName = enumerator.findCamera(options.deviceId, options.position)
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val deviceId = options.deviceId
var targetDeviceName: String? = null
if (deviceId != null) {
targetDeviceName = findCameraById(cameraManager, deviceId)
}
if (targetDeviceName == null) {
// Fallback to enumerator.findCamera which can't find camera by physical id but it will choose the closest one.
targetDeviceName = enumerator.findCamera(deviceId, options.position)
}
val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) as CameraXCapturer
return CameraXCapturerWithSize(
targetVideoCapturer,
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,
cameraManager,
targetDeviceName,
eventsHandler,
)
... ... @@ -75,6 +85,23 @@ class CameraXHelper {
override fun isSupported(context: Context): Boolean {
return Camera2Enumerator.isSupported(context) && lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)
}
private fun findCameraById(cameraManager: CameraManager, deviceId: String): String? {
for (id in cameraManager.cameraIdList) {
if (id == deviceId) return id // This means the provided id is logical id.
val characteristics = cameraManager.getCameraCharacteristics(id)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val ids = characteristics.physicalCameraIds
if (ids.contains(deviceId)) {
// This means the provided id is physical id.
enumerator?.physicalCameraId = deviceId
return id // This is its logical id.
}
}
}
return null
}
}
private fun getSupportedFormats(
... ...
... ... @@ -24,6 +24,7 @@ import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_
import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF
import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
import android.hardware.camera2.CaptureRequest
import android.os.Build
import android.os.Handler
import android.util.Range
import android.util.Size
... ... @@ -61,6 +62,7 @@ internal constructor(
private val height: Int,
private val frameRate: Int,
private val useCases: Array<out UseCase> = emptyArray(),
var physicalCameraId: String? = null,
) : CameraSession {
private var state = SessionState.RUNNING
... ... @@ -206,6 +208,13 @@ internal constructor(
private fun <T> ExtendableBuilder<T>.applyCameraSettings(): ExtendableBuilder<T> {
val cameraExtender = Camera2Interop.Extender(this)
physicalCameraId?.let { physicalId ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
cameraExtender.setPhysicalCameraId(physicalId)
}
}
val captureFormat = this@CameraXSession.captureFormat ?: return this
cameraExtender.setCaptureRequestOption(
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
... ...