Kasem Mohamed
Committed by GitHub

Refactor and enable switching between cameras using physical id. (#676)

* Refactor and enable switching between cameras using physical id.

* Override cameraName

* Rename unused arguments in predicate

* Update documentation

* Add changeset

* Move camera lookup to CameraXSession

* Override isBackFacing and isFrontFacing

* update byte buddy to support java 21

* Refactor to fix unit tests

* Run spotlessApply

* Run spotlessApply on all plugins

* cleanup

---------

Co-authored-by: davidliu <davidliu@deviange.net>
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +Fix switchCamera not working if the camera id is physical id
@@ -94,6 +94,7 @@ mockito-core = { module = "org.mockito:mockito-core", version = "4.11.0" } @@ -94,6 +94,7 @@ mockito-core = { module = "org.mockito:mockito-core", version = "4.11.0" }
94 mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "4.1.0" } 94 mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "4.1.0" }
95 #noinspection GradleDependency 95 #noinspection GradleDependency
96 mockito-inline = { module = "org.mockito:mockito-inline", version = "4.11.0" } 96 mockito-inline = { module = "org.mockito:mockito-inline", version = "4.11.0" }
  97 +byte-buddy = { module = "net.bytebuddy:byte-buddy", version = "1.14.3" }
97 98
98 robolectric = { module = "org.robolectric:robolectric", version = "4.14.1" } 99 robolectric = { module = "org.robolectric:robolectric", version = "4.14.1" }
99 turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" } 100 turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" }
@@ -32,13 +32,12 @@ import kotlinx.coroutines.flow.StateFlow @@ -32,13 +32,12 @@ import kotlinx.coroutines.flow.StateFlow
32 32
33 @ExperimentalCamera2Interop 33 @ExperimentalCamera2Interop
34 internal class CameraXCapturer( 34 internal class CameraXCapturer(
35 - context: Context, 35 + enumerator: CameraXEnumerator,
36 private val lifecycleOwner: LifecycleOwner, 36 private val lifecycleOwner: LifecycleOwner,
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,  
41 -) : CameraCapturer(cameraName, eventsHandler, CameraXEnumerator(context, lifecycleOwner)) { 40 +) : CameraCapturer(cameraName, eventsHandler, enumerator) {
42 41
43 @FlowObservable 42 @FlowObservable
44 @get:FlowObservable 43 @get:FlowObservable
@@ -94,7 +93,6 @@ internal class CameraXCapturer( @@ -94,7 +93,6 @@ internal class CameraXCapturer(
94 height, 93 height,
95 framerate, 94 framerate,
96 useCases, 95 useCases,
97 - physicalCameraId,  
98 ) 96 )
99 } 97 }
100 } 98 }
@@ -35,14 +35,40 @@ class CameraXEnumerator( @@ -35,14 +35,40 @@ 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,  
39 ) : Camera2Enumerator(context) { 38 ) : Camera2Enumerator(context) {
40 39
  40 + override fun getDeviceNames(): Array<out String?> {
  41 + val cm = cameraManager!!
  42 + val availableCameraIds = ArrayList<String>()
  43 + for (id in cm.cameraIdList) {
  44 + availableCameraIds.add(id)
  45 + if (VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  46 + val characteristics = cm.getCameraCharacteristics(id)
  47 + for (physicalId in characteristics.physicalCameraIds) {
  48 + availableCameraIds.add(physicalId)
  49 + }
  50 + }
  51 + }
  52 + return availableCameraIds.toTypedArray()
  53 + }
  54 +
  55 + override fun isBackFacing(deviceName: String?): Boolean {
  56 + val characteristics = cameraManager!!.getCameraCharacteristics(deviceName!!)
  57 + val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)
  58 + return lensFacing == CameraCharacteristics.LENS_FACING_BACK
  59 + }
  60 +
  61 + override fun isFrontFacing(deviceName: String?): Boolean {
  62 + val characteristics = cameraManager!!.getCameraCharacteristics(deviceName!!)
  63 + val lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING)
  64 + return lensFacing == CameraCharacteristics.LENS_FACING_FRONT
  65 + }
  66 +
41 override fun createCapturer( 67 override fun createCapturer(
42 deviceName: String?, 68 deviceName: String?,
43 eventsHandler: CameraVideoCapturer.CameraEventsHandler?, 69 eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
44 ): CameraVideoCapturer { 70 ): CameraVideoCapturer {
45 - return CameraXCapturer(context, lifecycleOwner, deviceName, eventsHandler, useCases, physicalCameraId) 71 + return CameraXCapturer(this, lifecycleOwner, deviceName, eventsHandler, useCases)
46 } 72 }
47 73
48 companion object { 74 companion object {
@@ -18,7 +18,6 @@ package livekit.org.webrtc @@ -18,7 +18,6 @@ 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  
22 import androidx.camera.camera2.interop.ExperimentalCamera2Interop 21 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
23 import androidx.camera.core.UseCase 22 import androidx.camera.core.UseCase
24 import androidx.lifecycle.Lifecycle 23 import androidx.lifecycle.Lifecycle
@@ -63,21 +62,16 @@ class CameraXHelper { @@ -63,21 +62,16 @@ class CameraXHelper {
63 ): VideoCapturer { 62 ): VideoCapturer {
64 val enumerator = provideEnumerator(context) 63 val enumerator = provideEnumerator(context)
65 val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager 64 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 - }  
75 - val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) as CameraXCapturer 65 +
  66 + val targetDevice = enumerator.findCamera(options.deviceId, options.position)
  67 + val targetDeviceId = targetDevice?.deviceId
  68 +
  69 + val targetVideoCapturer = enumerator.createCapturer(targetDeviceId, eventsHandler) as CameraXCapturer
76 70
77 return CameraXCapturerWithSize( 71 return CameraXCapturerWithSize(
78 targetVideoCapturer, 72 targetVideoCapturer,
79 cameraManager, 73 cameraManager,
80 - targetDeviceName, 74 + targetDeviceId,
81 eventsHandler, 75 eventsHandler,
82 ) 76 )
83 } 77 }
@@ -85,23 +79,6 @@ class CameraXHelper { @@ -85,23 +79,6 @@ class CameraXHelper {
85 override fun isSupported(context: Context): Boolean { 79 override fun isSupported(context: Context): Boolean {
86 return Camera2Enumerator.isSupported(context) && lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED) 80 return Camera2Enumerator.isSupported(context) && lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)
87 } 81 }
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 - }  
105 } 82 }
106 83
107 private fun getSupportedFormats( 84 private fun getSupportedFormats(
@@ -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.CameraCharacteristics 20 import android.hardware.camera2.CameraCharacteristics
  21 +import android.hardware.camera2.CameraManager
21 import android.hardware.camera2.CameraMetadata 22 import android.hardware.camera2.CameraMetadata
22 import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF 23 import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
23 import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON 24 import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON
@@ -25,6 +26,7 @@ import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_O @@ -25,6 +26,7 @@ import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_O
25 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON 26 import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
26 import android.hardware.camera2.CaptureRequest 27 import android.hardware.camera2.CaptureRequest
27 import android.os.Build 28 import android.os.Build
  29 +import android.os.Build.VERSION
28 import android.os.Handler 30 import android.os.Handler
29 import android.util.Range 31 import android.util.Range
30 import android.util.Size 32 import android.util.Size
@@ -62,7 +64,6 @@ internal constructor( @@ -62,7 +64,6 @@ internal constructor(
62 private val height: Int, 64 private val height: Int,
63 private val frameRate: Int, 65 private val frameRate: Int,
64 private val useCases: Array<out UseCase> = emptyArray(), 66 private val useCases: Array<out UseCase> = emptyArray(),
65 - var physicalCameraId: String? = null,  
66 ) : CameraSession { 67 ) : CameraSession {
67 68
68 private var state = SessionState.RUNNING 69 private var state = SessionState.RUNNING
@@ -106,6 +107,13 @@ internal constructor( @@ -106,6 +107,13 @@ internal constructor(
106 } 107 }
107 } 108 }
108 109
  110 + private val cameraDevice: CameraDeviceId
  111 + get() {
  112 + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
  113 + return findCamera(cameraManager, cameraId)
  114 + ?: throw IllegalArgumentException("Camera ID $cameraId not found")
  115 + }
  116 +
109 init { 117 init {
110 cameraThreadHandler.post { 118 cameraThreadHandler.post {
111 start() 119 start()
@@ -160,7 +168,7 @@ internal constructor( @@ -160,7 +168,7 @@ internal constructor(
160 168
161 // Select camera by ID 169 // Select camera by ID
162 val cameraSelector = CameraSelector.Builder() 170 val cameraSelector = CameraSelector.Builder()
163 - .addCameraFilter { cameraInfo -> cameraInfo.filter { Camera2CameraInfo.from(it).cameraId == cameraId } } 171 + .addCameraFilter { cameraInfo -> cameraInfo.filter { Camera2CameraInfo.from(it).cameraId == cameraDevice.deviceId } }
164 .build() 172 .build()
165 173
166 try { 174 try {
@@ -209,7 +217,7 @@ internal constructor( @@ -209,7 +217,7 @@ internal constructor(
209 private fun <T> ExtendableBuilder<T>.applyCameraSettings(): ExtendableBuilder<T> { 217 private fun <T> ExtendableBuilder<T>.applyCameraSettings(): ExtendableBuilder<T> {
210 val cameraExtender = Camera2Interop.Extender(this) 218 val cameraExtender = Camera2Interop.Extender(this)
211 219
212 - physicalCameraId?.let { physicalId -> 220 + cameraDevice.physicalId?.let { physicalId ->
213 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 221 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
214 cameraExtender.setPhysicalCameraId(physicalId) 222 cameraExtender.setPhysicalCameraId(physicalId)
215 } 223 }
@@ -275,7 +283,7 @@ internal constructor( @@ -275,7 +283,7 @@ internal constructor(
275 } 283 }
276 284
277 private fun obtainCameraConfiguration() { 285 private fun obtainCameraConfiguration() {
278 - val camera = cameraProvider.availableCameraInfos.map { Camera2CameraInfo.from(it) }.first { it.cameraId == cameraId } 286 + val camera = cameraProvider.availableCameraInfos.map { Camera2CameraInfo.from(it) }.first { it.cameraId == cameraDevice.deviceId }
279 287
280 cameraOrientation = camera.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION) ?: -1 288 cameraOrientation = camera.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION) ?: -1
281 isCameraFrontFacing = camera.getCameraCharacteristic(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT 289 isCameraFrontFacing = camera.getCameraCharacteristic(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT
@@ -326,6 +334,30 @@ internal constructor( @@ -326,6 +334,30 @@ internal constructor(
326 return (cameraOrientation + rotation) % 360 334 return (cameraOrientation + rotation) % 360
327 } 335 }
328 336
  337 + private data class CameraDeviceId(val deviceId: String, val physicalId: String?)
  338 +
  339 + private fun findCamera(
  340 + cameraManager: CameraManager,
  341 + deviceId: String,
  342 + ): CameraDeviceId? {
  343 + for (id in cameraManager.cameraIdList) {
  344 + // First check if deviceId is a direct logical camera ID
  345 + if (id == deviceId) return CameraDeviceId(id, null)
  346 +
  347 + // Then check if deviceId is a physical camera ID in a logical camera
  348 + if (VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  349 + val characteristic = cameraManager.getCameraCharacteristics(id)
  350 +
  351 + for (physicalId in characteristic.physicalCameraIds) {
  352 + if (deviceId == physicalId) {
  353 + return CameraDeviceId(id, physicalId)
  354 + }
  355 + }
  356 + }
  357 + }
  358 + return null
  359 + }
  360 +
329 companion object { 361 companion object {
330 private const val TAG = "CameraXSession" 362 private const val TAG = "CameraXSession"
331 private val cameraXStartTimeMsHistogram = Histogram.createCounts("WebRTC.Android.CameraX.StartTimeMs", 1, 10000, 50) 363 private val cameraXStartTimeMsHistogram = Histogram.createCounts("WebRTC.Android.CameraX.StartTimeMs", 1, 10000, 50)
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.
@@ -27,9 +27,9 @@ import io.livekit.android.memory.CloseableManager @@ -27,9 +27,9 @@ import io.livekit.android.memory.CloseableManager
27 import io.livekit.android.memory.SurfaceTextureHelperCloser 27 import io.livekit.android.memory.SurfaceTextureHelperCloser
28 import io.livekit.android.room.DefaultsManager 28 import io.livekit.android.room.DefaultsManager
29 import io.livekit.android.room.track.video.CameraCapturerUtils 29 import io.livekit.android.room.track.video.CameraCapturerUtils
  30 +import io.livekit.android.room.track.video.CameraCapturerUtils.CameraDeviceInfo
30 import io.livekit.android.room.track.video.CameraCapturerUtils.createCameraEnumerator 31 import io.livekit.android.room.track.video.CameraCapturerUtils.createCameraEnumerator
31 import io.livekit.android.room.track.video.CameraCapturerUtils.findCamera 32 import io.livekit.android.room.track.video.CameraCapturerUtils.findCamera
32 -import io.livekit.android.room.track.video.CameraCapturerUtils.getCameraPosition  
33 import io.livekit.android.room.track.video.CameraCapturerWithSize 33 import io.livekit.android.room.track.video.CameraCapturerWithSize
34 import io.livekit.android.room.track.video.CaptureDispatchObserver 34 import io.livekit.android.room.track.video.CaptureDispatchObserver
35 import io.livekit.android.room.track.video.ScaleCropVideoProcessor 35 import io.livekit.android.room.track.video.ScaleCropVideoProcessor
@@ -179,26 +179,28 @@ constructor( @@ -179,26 +179,28 @@ constructor(
179 return 179 return
180 } 180 }
181 181
182 - var targetDeviceId: String? = null 182 + var targetDevice: CameraDeviceInfo? = null
183 val enumerator = createCameraEnumerator(context) 183 val enumerator = createCameraEnumerator(context)
184 if (deviceId != null || position != null) { 184 if (deviceId != null || position != null) {
185 - targetDeviceId = enumerator.findCamera(deviceId, position, fallback = false) 185 + targetDevice = enumerator.findCamera(deviceId, position, fallback = false)
186 } 186 }
187 187
188 - if (targetDeviceId == null) { 188 + if (targetDevice == null) {
189 val deviceNames = enumerator.deviceNames 189 val deviceNames = enumerator.deviceNames
190 if (deviceNames.size < 2) { 190 if (deviceNames.size < 2) {
191 LKLog.w { "No available cameras to switch to!" } 191 LKLog.w { "No available cameras to switch to!" }
192 return 192 return
193 } 193 }
194 val currentIndex = deviceNames.indexOf(options.deviceId) 194 val currentIndex = deviceNames.indexOf(options.deviceId)
195 - targetDeviceId = deviceNames[(currentIndex + 1) % deviceNames.size] 195 + val targetDeviceId = deviceNames[(currentIndex + 1) % deviceNames.size]
  196 + targetDevice = enumerator.findCamera(targetDeviceId, fallback = false)
196 } 197 }
197 198
  199 + val targetDeviceId = targetDevice?.deviceId
198 fun updateCameraOptions() { 200 fun updateCameraOptions() {
199 val newOptions = options.copy( 201 val newOptions = options.copy(
200 deviceId = targetDeviceId, 202 deviceId = targetDeviceId,
201 - position = enumerator.getCameraPosition(targetDeviceId), 203 + position = targetDevice?.position,
202 ) 204 )
203 options = newOptions 205 options = newOptions
204 } 206 }
@@ -243,7 +245,7 @@ constructor( @@ -243,7 +245,7 @@ constructor(
243 LKLog.w { "switching camera failed: $errorDescription" } 245 LKLog.w { "switching camera failed: $errorDescription" }
244 } 246 }
245 } 247 }
246 - if (targetDeviceId == null) { 248 + if (targetDevice == null) {
247 LKLog.w { "No target camera found!" } 249 LKLog.w { "No target camera found!" }
248 return 250 return
249 } else { 251 } else {
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.
@@ -72,9 +72,7 @@ object CameraCapturerUtils { @@ -72,9 +72,7 @@ object CameraCapturerUtils {
72 private fun getCameraProvider(context: Context): CameraProvider { 72 private fun getCameraProvider(context: Context): CameraProvider {
73 return cameraProviders 73 return cameraProviders
74 .sortedByDescending { it.cameraVersion } 74 .sortedByDescending { it.cameraVersion }
75 - .first {  
76 - it.isSupported(context)  
77 - } 75 + .first { it.isSupported(context) }
78 } 76 }
79 77
80 /** 78 /**
@@ -98,15 +96,15 @@ object CameraCapturerUtils { @@ -98,15 +96,15 @@ object CameraCapturerUtils {
98 provider: CameraProvider, 96 provider: CameraProvider,
99 options: LocalVideoTrackOptions, 97 options: LocalVideoTrackOptions,
100 ): Pair<VideoCapturer, LocalVideoTrackOptions>? { 98 ): Pair<VideoCapturer, LocalVideoTrackOptions>? {
101 - val cameraEventsDispatchHandler = CameraEventsDispatchHandler()  
102 val cameraEnumerator = provider.provideEnumerator(context) 99 val cameraEnumerator = provider.provideEnumerator(context)
103 - val targetDeviceName = cameraEnumerator.findCamera(options.deviceId, options.position) ?: return null 100 + val cameraEventsDispatchHandler = CameraEventsDispatchHandler()
  101 + val targetDevice = cameraEnumerator.findCamera(options.deviceId, options.position) ?: return null
104 val targetVideoCapturer = provider.provideCapturer(context, options, cameraEventsDispatchHandler) 102 val targetVideoCapturer = provider.provideCapturer(context, options, cameraEventsDispatchHandler)
105 103
106 // back fill any missing information 104 // back fill any missing information
107 val newOptions = options.copy( 105 val newOptions = options.copy(
108 - deviceId = targetDeviceName,  
109 - position = cameraEnumerator.getCameraPosition(targetDeviceName), 106 + deviceId = targetDevice.deviceId,
  107 + position = targetDevice.position,
110 ) 108 )
111 109
112 if (targetVideoCapturer !is VideoCapturerWithSize) { 110 if (targetVideoCapturer !is VideoCapturerWithSize) {
@@ -130,13 +128,13 @@ object CameraCapturerUtils { @@ -130,13 +128,13 @@ object CameraCapturerUtils {
130 options: LocalVideoTrackOptions, 128 options: LocalVideoTrackOptions,
131 eventsHandler: CameraEventsDispatchHandler, 129 eventsHandler: CameraEventsDispatchHandler,
132 ): VideoCapturer { 130 ): VideoCapturer {
133 - val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) 131 + val targetDevice = enumerator.findCamera(options.deviceId, options.position)
134 // Cache supported capture formats ahead of time to avoid future camera locks. 132 // Cache supported capture formats ahead of time to avoid future camera locks.
135 - Camera1Helper.getSupportedFormats(Camera1Helper.getCameraId(targetDeviceName))  
136 - val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) 133 + Camera1Helper.getSupportedFormats(Camera1Helper.getCameraId(targetDevice?.deviceId))
  134 + val targetVideoCapturer = enumerator.createCapturer(targetDevice?.deviceId, eventsHandler)
137 return Camera1CapturerWithSize( 135 return Camera1CapturerWithSize(
138 targetVideoCapturer as Camera1Capturer, 136 targetVideoCapturer as Camera1Capturer,
139 - targetDeviceName, 137 + targetDevice?.deviceId,
140 eventsHandler, 138 eventsHandler,
141 ) 139 )
142 } 140 }
@@ -149,8 +147,7 @@ object CameraCapturerUtils { @@ -149,8 +147,7 @@ object CameraCapturerUtils {
149 147
150 override val cameraVersion = 2 148 override val cameraVersion = 2
151 149
152 - override fun provideEnumerator(context: Context): CameraEnumerator =  
153 - enumerator ?: Camera2Enumerator(context).also { 150 + override fun provideEnumerator(context: Context): CameraEnumerator = enumerator ?: Camera2Enumerator(context).also {
154 enumerator = it 151 enumerator = it
155 } 152 }
156 153
@@ -160,12 +157,12 @@ object CameraCapturerUtils { @@ -160,12 +157,12 @@ object CameraCapturerUtils {
160 eventsHandler: CameraEventsDispatchHandler, 157 eventsHandler: CameraEventsDispatchHandler,
161 ): VideoCapturer { 158 ): VideoCapturer {
162 val enumerator = provideEnumerator(context) 159 val enumerator = provideEnumerator(context)
163 - val targetDeviceName = enumerator.findCamera(options.deviceId, options.position)  
164 - val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) 160 + val targetDevice = enumerator.findCamera(options.deviceId, options.position)
  161 + val targetVideoCapturer = enumerator.createCapturer(targetDevice?.deviceId, eventsHandler)
165 return Camera2CapturerWithSize( 162 return Camera2CapturerWithSize(
166 targetVideoCapturer as Camera2Capturer, 163 targetVideoCapturer as Camera2Capturer,
167 context.getSystemService(Context.CAMERA_SERVICE) as CameraManager, 164 context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,
168 - targetDeviceName, 165 + targetDevice?.deviceId,
169 eventsHandler, 166 eventsHandler,
170 ) 167 )
171 } 168 }
@@ -184,55 +181,53 @@ object CameraCapturerUtils { @@ -184,55 +181,53 @@ object CameraCapturerUtils {
184 deviceId: String? = null, 181 deviceId: String? = null,
185 position: CameraPosition? = null, 182 position: CameraPosition? = null,
186 fallback: Boolean = true, 183 fallback: Boolean = true,
187 - ): String? {  
188 - var targetDeviceName: String? = null 184 + ): CameraDeviceInfo? {
  185 + var targetDevice: CameraDeviceInfo? = null
189 // Prioritize search by deviceId first 186 // Prioritize search by deviceId first
190 if (deviceId != null) { 187 if (deviceId != null) {
191 - targetDeviceName = findCamera { deviceName -> deviceName == deviceId } 188 + targetDevice = findCamera { id, _ ->
  189 + id == deviceId
  190 + }
192 } 191 }
193 192
194 // Search by camera position 193 // Search by camera position
195 - if (targetDeviceName == null && position != null) {  
196 - targetDeviceName = findCamera { deviceName ->  
197 - getCameraPosition(deviceName) == position 194 + if (targetDevice == null && position != null) {
  195 + targetDevice = findCamera { _, pos ->
  196 + pos == position
198 } 197 }
199 } 198 }
200 199
201 // Fall back by choosing first available camera. 200 // Fall back by choosing first available camera.
202 - if (targetDeviceName == null && fallback) {  
203 - targetDeviceName = findCamera { true } 201 + if (targetDevice == null && fallback) {
  202 + targetDevice = findCamera { _, _ -> true }
204 } 203 }
205 204
206 - if (targetDeviceName == null) {  
207 - return null 205 + return targetDevice
208 } 206 }
209 207
210 - return targetDeviceName  
211 - } 208 + data class CameraDeviceInfo(val deviceId: String, val position: CameraPosition?)
212 209
213 /** 210 /**
214 - * Finds the device id of a camera that matches the [predicate]. 211 + * Returns information about a camera by searching for the specified device ID.
  212 + *
  213 + * @param predicate with deviceId and position, return true if camera is found
  214 + * @return [CameraDeviceInfo] with camera id and position if found, null otherwise
215 */ 215 */
216 - fun CameraEnumerator.findCamera(predicate: (deviceName: String) -> Boolean): String? {  
217 - for (deviceName in deviceNames) {  
218 - if (predicate(deviceName)) {  
219 - return deviceName  
220 - }  
221 - }  
222 - return null 216 + fun CameraEnumerator.findCamera(
  217 + predicate: (deviceId: String, position: CameraPosition?) -> Boolean,
  218 + ): CameraDeviceInfo? {
  219 + for (id in deviceNames) {
  220 + val position = if (isFrontFacing(id)) {
  221 + CameraPosition.FRONT
  222 + } else if (isBackFacing(id)) {
  223 + CameraPosition.BACK
  224 + } else {
  225 + null
223 } 226 }
224 227
225 - /**  
226 - * Returns the camera position of a camera, or null if neither front or back facing (e.g. external camera).  
227 - */  
228 - fun CameraEnumerator.getCameraPosition(deviceName: String?): CameraPosition? {  
229 - if (deviceName == null) {  
230 - return null 228 + if (predicate(id, position)) {
  229 + return CameraDeviceInfo(id, position)
231 } 230 }
232 - if (isBackFacing(deviceName)) {  
233 - return CameraPosition.BACK  
234 - } else if (isFrontFacing(deviceName)) {  
235 - return CameraPosition.FRONT  
236 } 231 }
237 return null 232 return null
238 } 233 }
@@ -91,6 +91,7 @@ dependencies { @@ -91,6 +91,7 @@ dependencies {
91 implementation libs.androidx.test.core 91 implementation libs.androidx.test.core
92 implementation libs.coroutines.test 92 implementation libs.coroutines.test
93 implementation libs.dagger.lib 93 implementation libs.dagger.lib
  94 + implementation libs.byte.buddy
94 kapt libs.dagger.compiler 95 kapt libs.dagger.compiler
95 96
96 testImplementation libs.junit 97 testImplementation libs.junit