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>
  1 +---
  2 +"client-sdk-android": minor
  3 +---
  4 +
  5 +CameraX: support for selecting cameras by their physical id
1 /* 1 /*
2 - * Copyright 2024 LiveKit, Inc. 2 + * Copyright 2024-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ internal class CameraXCapturer( @@ -37,6 +37,7 @@ internal class CameraXCapturer(
37 cameraName: String?, 37 cameraName: String?,
38 eventsHandler: CameraVideoCapturer.CameraEventsHandler?, 38 eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
39 private val useCases: Array<out UseCase> = emptyArray(), 39 private val useCases: Array<out UseCase> = emptyArray(),
  40 + var physicalCameraId: String? = null,
40 ) : CameraCapturer(cameraName, eventsHandler, CameraXEnumerator(context, lifecycleOwner)) { 41 ) : CameraCapturer(cameraName, eventsHandler, CameraXEnumerator(context, lifecycleOwner)) {
41 42
42 @FlowObservable 43 @FlowObservable
@@ -93,6 +94,7 @@ internal class CameraXCapturer( @@ -93,6 +94,7 @@ internal class CameraXCapturer(
93 height, 94 height,
94 framerate, 95 framerate,
95 useCases, 96 useCases,
  97 + physicalCameraId,
96 ) 98 )
97 } 99 }
98 } 100 }
1 /* 1 /*
2 - * Copyright 2024 LiveKit, Inc. 2 + * Copyright 2024-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -35,13 +35,14 @@ class CameraXEnumerator( @@ -35,13 +35,14 @@ class CameraXEnumerator(
35 context: Context, 35 context: Context,
36 private val lifecycleOwner: LifecycleOwner, 36 private val lifecycleOwner: LifecycleOwner,
37 private val useCases: Array<out UseCase> = emptyArray(), 37 private val useCases: Array<out UseCase> = emptyArray(),
  38 + var physicalCameraId: String? = null,
38 ) : Camera2Enumerator(context) { 39 ) : Camera2Enumerator(context) {
39 40
40 override fun createCapturer( 41 override fun createCapturer(
41 deviceName: String?, 42 deviceName: String?,
42 eventsHandler: CameraVideoCapturer.CameraEventsHandler?, 43 eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
43 ): CameraVideoCapturer { 44 ): CameraVideoCapturer {
44 - return CameraXCapturer(context, lifecycleOwner, deviceName, eventsHandler, useCases) 45 + return CameraXCapturer(context, lifecycleOwner, deviceName, eventsHandler, useCases, physicalCameraId)
45 } 46 }
46 47
47 companion object { 48 companion object {
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package livekit.org.webrtc @@ -18,6 +18,7 @@ package livekit.org.webrtc
18 18
19 import android.content.Context 19 import android.content.Context
20 import android.hardware.camera2.CameraManager 20 import android.hardware.camera2.CameraManager
  21 +import android.os.Build
21 import androidx.camera.camera2.interop.ExperimentalCamera2Interop 22 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
22 import androidx.camera.core.UseCase 23 import androidx.camera.core.UseCase
23 import androidx.lifecycle.Lifecycle 24 import androidx.lifecycle.Lifecycle
@@ -61,12 +62,21 @@ class CameraXHelper { @@ -61,12 +62,21 @@ class CameraXHelper {
61 eventsHandler: CameraEventsDispatchHandler, 62 eventsHandler: CameraEventsDispatchHandler,
62 ): VideoCapturer { 63 ): VideoCapturer {
63 val enumerator = provideEnumerator(context) 64 val enumerator = provideEnumerator(context)
64 - val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) 65 + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
  66 + val deviceId = options.deviceId
  67 + var targetDeviceName: String? = null
  68 + if (deviceId != null) {
  69 + targetDeviceName = findCameraById(cameraManager, deviceId)
  70 + }
  71 + if (targetDeviceName == null) {
  72 + // Fallback to enumerator.findCamera which can't find camera by physical id but it will choose the closest one.
  73 + targetDeviceName = enumerator.findCamera(deviceId, options.position)
  74 + }
65 val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) as CameraXCapturer 75 val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) as CameraXCapturer
66 76
67 return CameraXCapturerWithSize( 77 return CameraXCapturerWithSize(
68 targetVideoCapturer, 78 targetVideoCapturer,
69 - context.getSystemService(Context.CAMERA_SERVICE) as CameraManager, 79 + cameraManager,
70 targetDeviceName, 80 targetDeviceName,
71 eventsHandler, 81 eventsHandler,
72 ) 82 )
@@ -75,6 +85,23 @@ class CameraXHelper { @@ -75,6 +85,23 @@ class CameraXHelper {
75 override fun isSupported(context: Context): Boolean { 85 override fun isSupported(context: Context): Boolean {
76 return Camera2Enumerator.isSupported(context) && lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED) 86 return Camera2Enumerator.isSupported(context) && lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)
77 } 87 }
  88 +
  89 + private fun findCameraById(cameraManager: CameraManager, deviceId: String): String? {
  90 + for (id in cameraManager.cameraIdList) {
  91 + if (id == deviceId) return id // This means the provided id is logical id.
  92 +
  93 + val characteristics = cameraManager.getCameraCharacteristics(id)
  94 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  95 + val ids = characteristics.physicalCameraIds
  96 + if (ids.contains(deviceId)) {
  97 + // This means the provided id is physical id.
  98 + enumerator?.physicalCameraId = deviceId
  99 + return id // This is its logical id.
  100 + }
  101 + }
  102 + }
  103 + return null
  104 + }
78 } 105 }
79 106
80 private fun getSupportedFormats( 107 private fun getSupportedFormats(
@@ -24,6 +24,7 @@ import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ @@ -24,6 +24,7 @@ import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_
24 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF 24 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF
25 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON 25 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
26 import android.hardware.camera2.CaptureRequest 26 import android.hardware.camera2.CaptureRequest
  27 +import android.os.Build
27 import android.os.Handler 28 import android.os.Handler
28 import android.util.Range 29 import android.util.Range
29 import android.util.Size 30 import android.util.Size
@@ -61,6 +62,7 @@ internal constructor( @@ -61,6 +62,7 @@ internal constructor(
61 private val height: Int, 62 private val height: Int,
62 private val frameRate: Int, 63 private val frameRate: Int,
63 private val useCases: Array<out UseCase> = emptyArray(), 64 private val useCases: Array<out UseCase> = emptyArray(),
  65 + var physicalCameraId: String? = null,
64 ) : CameraSession { 66 ) : CameraSession {
65 67
66 private var state = SessionState.RUNNING 68 private var state = SessionState.RUNNING
@@ -206,6 +208,13 @@ internal constructor( @@ -206,6 +208,13 @@ internal constructor(
206 208
207 private fun <T> ExtendableBuilder<T>.applyCameraSettings(): ExtendableBuilder<T> { 209 private fun <T> ExtendableBuilder<T>.applyCameraSettings(): ExtendableBuilder<T> {
208 val cameraExtender = Camera2Interop.Extender(this) 210 val cameraExtender = Camera2Interop.Extender(this)
  211 +
  212 + physicalCameraId?.let { physicalId ->
  213 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  214 + cameraExtender.setPhysicalCameraId(physicalId)
  215 + }
  216 + }
  217 +
209 val captureFormat = this@CameraXSession.captureFormat ?: return this 218 val captureFormat = this@CameraXSession.captureFormat ?: return this
210 cameraExtender.setCaptureRequestOption( 219 cameraExtender.setCaptureRequestOption(
211 CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, 220 CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,