Skip to content

Commit 5291759

Browse files
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 parent c39cc41 commit 5291759

File tree

9 files changed

+135
-98
lines changed

9 files changed

+135
-98
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Fix switchCamera not working if the camera id is physical id

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ mockito-core = { module = "org.mockito:mockito-core", version = "4.11.0" }
9494
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "4.1.0" }
9595
#noinspection GradleDependency
9696
mockito-inline = { module = "org.mockito:mockito-inline", version = "4.11.0" }
97+
byte-buddy = { module = "net.bytebuddy:byte-buddy", version = "1.14.3" }
9798

9899
robolectric = { module = "org.robolectric:robolectric", version = "4.14.1" }
99100
turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" }

livekit-android-camerax/src/main/java/livekit/org/webrtc/CameraXCapturer.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@ import kotlinx.coroutines.flow.StateFlow
3232

3333
@ExperimentalCamera2Interop
3434
internal class CameraXCapturer(
35-
context: Context,
35+
enumerator: CameraXEnumerator,
3636
private val lifecycleOwner: LifecycleOwner,
3737
cameraName: String?,
3838
eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
3939
private val useCases: Array<out UseCase> = emptyArray(),
40-
var physicalCameraId: String? = null,
41-
) : CameraCapturer(cameraName, eventsHandler, CameraXEnumerator(context, lifecycleOwner)) {
40+
) : CameraCapturer(cameraName, eventsHandler, enumerator) {
4241

4342
@FlowObservable
4443
@get:FlowObservable
@@ -94,7 +93,6 @@ internal class CameraXCapturer(
9493
height,
9594
framerate,
9695
useCases,
97-
physicalCameraId,
9896
)
9997
}
10098
}

livekit-android-camerax/src/main/java/livekit/org/webrtc/CameraXEnumerator.kt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,40 @@ class CameraXEnumerator(
3535
context: Context,
3636
private val lifecycleOwner: LifecycleOwner,
3737
private val useCases: Array<out UseCase> = emptyArray(),
38-
var physicalCameraId: String? = null,
3938
) : Camera2Enumerator(context) {
4039

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+
4167
override fun createCapturer(
4268
deviceName: String?,
4369
eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
4470
): CameraVideoCapturer {
45-
return CameraXCapturer(context, lifecycleOwner, deviceName, eventsHandler, useCases, physicalCameraId)
71+
return CameraXCapturer(this, lifecycleOwner, deviceName, eventsHandler, useCases)
4672
}
4773

4874
companion object {

livekit-android-camerax/src/main/java/livekit/org/webrtc/CameraXHelper.kt

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package livekit.org.webrtc
1818

1919
import android.content.Context
2020
import android.hardware.camera2.CameraManager
21-
import android.os.Build
2221
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
2322
import androidx.camera.core.UseCase
2423
import androidx.lifecycle.Lifecycle
@@ -63,45 +62,23 @@ class CameraXHelper {
6362
): VideoCapturer {
6463
val enumerator = provideEnumerator(context)
6564
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
7670

7771
return CameraXCapturerWithSize(
7872
targetVideoCapturer,
7973
cameraManager,
80-
targetDeviceName,
74+
targetDeviceId,
8175
eventsHandler,
8276
)
8377
}
8478

8579
override fun isSupported(context: Context): Boolean {
8680
return Camera2Enumerator.isSupported(context) && lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)
8781
}
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-
}
10582
}
10683

10784
private fun getSupportedFormats(

livekit-android-camerax/src/main/java/livekit/org/webrtc/CameraXSession.kt

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ package livekit.org.webrtc
1818

1919
import android.content.Context
2020
import android.hardware.camera2.CameraCharacteristics
21+
import android.hardware.camera2.CameraManager
2122
import android.hardware.camera2.CameraMetadata
2223
import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
2324
import android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON
2425
import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF
2526
import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
2627
import android.hardware.camera2.CaptureRequest
2728
import android.os.Build
29+
import android.os.Build.VERSION
2830
import android.os.Handler
2931
import android.util.Range
3032
import android.util.Size
@@ -62,7 +64,6 @@ internal constructor(
6264
private val height: Int,
6365
private val frameRate: Int,
6466
private val useCases: Array<out UseCase> = emptyArray(),
65-
var physicalCameraId: String? = null,
6667
) : CameraSession {
6768

6869
private var state = SessionState.RUNNING
@@ -106,6 +107,13 @@ internal constructor(
106107
}
107108
}
108109

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+
109117
init {
110118
cameraThreadHandler.post {
111119
start()
@@ -160,7 +168,7 @@ internal constructor(
160168

161169
// Select camera by ID
162170
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 } }
164172
.build()
165173

166174
try {
@@ -209,7 +217,7 @@ internal constructor(
209217
private fun <T> ExtendableBuilder<T>.applyCameraSettings(): ExtendableBuilder<T> {
210218
val cameraExtender = Camera2Interop.Extender(this)
211219

212-
physicalCameraId?.let { physicalId ->
220+
cameraDevice.physicalId?.let { physicalId ->
213221
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
214222
cameraExtender.setPhysicalCameraId(physicalId)
215223
}
@@ -275,7 +283,7 @@ internal constructor(
275283
}
276284

277285
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 }
279287

280288
cameraOrientation = camera.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION) ?: -1
281289
isCameraFrontFacing = camera.getCameraCharacteristic(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT
@@ -326,6 +334,30 @@ internal constructor(
326334
return (cameraOrientation + rotation) % 360
327335
}
328336

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+
329361
companion object {
330362
private const val TAG = "CameraXSession"
331363
private val cameraXStartTimeMsHistogram = Histogram.createCounts("WebRTC.Android.CameraX.StartTimeMs", 1, 10000, 50)

livekit-android-sdk/src/main/java/io/livekit/android/room/track/LocalVideoTrack.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 LiveKit, Inc.
2+
* Copyright 2023-2025 LiveKit, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,9 +27,9 @@ import io.livekit.android.memory.CloseableManager
2727
import io.livekit.android.memory.SurfaceTextureHelperCloser
2828
import io.livekit.android.room.DefaultsManager
2929
import io.livekit.android.room.track.video.CameraCapturerUtils
30+
import io.livekit.android.room.track.video.CameraCapturerUtils.CameraDeviceInfo
3031
import io.livekit.android.room.track.video.CameraCapturerUtils.createCameraEnumerator
3132
import io.livekit.android.room.track.video.CameraCapturerUtils.findCamera
32-
import io.livekit.android.room.track.video.CameraCapturerUtils.getCameraPosition
3333
import io.livekit.android.room.track.video.CameraCapturerWithSize
3434
import io.livekit.android.room.track.video.CaptureDispatchObserver
3535
import io.livekit.android.room.track.video.ScaleCropVideoProcessor
@@ -179,26 +179,28 @@ constructor(
179179
return
180180
}
181181

182-
var targetDeviceId: String? = null
182+
var targetDevice: CameraDeviceInfo? = null
183183
val enumerator = createCameraEnumerator(context)
184184
if (deviceId != null || position != null) {
185-
targetDeviceId = enumerator.findCamera(deviceId, position, fallback = false)
185+
targetDevice = enumerator.findCamera(deviceId, position, fallback = false)
186186
}
187187

188-
if (targetDeviceId == null) {
188+
if (targetDevice == null) {
189189
val deviceNames = enumerator.deviceNames
190190
if (deviceNames.size < 2) {
191191
LKLog.w { "No available cameras to switch to!" }
192192
return
193193
}
194194
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)
196197
}
197198

199+
val targetDeviceId = targetDevice?.deviceId
198200
fun updateCameraOptions() {
199201
val newOptions = options.copy(
200202
deviceId = targetDeviceId,
201-
position = enumerator.getCameraPosition(targetDeviceId),
203+
position = targetDevice?.position,
202204
)
203205
options = newOptions
204206
}
@@ -243,7 +245,7 @@ constructor(
243245
LKLog.w { "switching camera failed: $errorDescription" }
244246
}
245247
}
246-
if (targetDeviceId == null) {
248+
if (targetDevice == null) {
247249
LKLog.w { "No target camera found!" }
248250
return
249251
} else {

0 commit comments

Comments
 (0)