davidliu
Committed by GitHub

Switch camera api for changing cameras directly (#149)

restartTrack has some issues on specific devices, causing camera errors.
@@ -77,6 +77,60 @@ constructor( @@ -77,6 +77,60 @@ constructor(
77 restartTrack(options.copy(deviceId = deviceId)) 77 restartTrack(options.copy(deviceId = deviceId))
78 } 78 }
79 79
  80 + /**
  81 + * Switch to a different camera. Only works if this track is backed by a camera capturer.
  82 + *
  83 + * If neither deviceId or position is provided, or the specified camera cannot be found,
  84 + * this will switch to the next camera, if one is available.
  85 + */
  86 + fun switchCamera(deviceId: String? = null, position: CameraPosition? = null) {
  87 +
  88 + val cameraCapturer = capturer as? CameraVideoCapturer ?: run {
  89 + LKLog.w { "Attempting to switch camera on a non-camera video track!" }
  90 + return
  91 + }
  92 +
  93 + var targetDeviceId: String? = null
  94 + val enumerator = createCameraEnumerator(context)
  95 + if (deviceId != null || position != null) {
  96 + targetDeviceId = enumerator.findCamera(deviceId, position, fallback = false)
  97 + }
  98 +
  99 + if (targetDeviceId == null) {
  100 + val deviceNames = enumerator.deviceNames
  101 + if (deviceNames.size < 2) {
  102 + LKLog.w { "No available cameras to switch to!" }
  103 + return
  104 + }
  105 + val currentIndex = deviceNames.indexOf(options.deviceId)
  106 + targetDeviceId = deviceNames[(currentIndex + 1) % deviceNames.size]
  107 + }
  108 +
  109 + val cameraSwitchHandler = object : CameraVideoCapturer.CameraSwitchHandler {
  110 + override fun onCameraSwitchDone(isFrontFacing: Boolean) {
  111 + val newOptions = options.copy(
  112 + deviceId = targetDeviceId,
  113 + position = enumerator.getCameraPosition(targetDeviceId)
  114 + )
  115 + options = newOptions
  116 + }
  117 +
  118 + override fun onCameraSwitchError(errorDescription: String?) {
  119 + LKLog.w { "switching camera failed: $errorDescription" }
  120 + }
  121 +
  122 + }
  123 + if (targetDeviceId == null) {
  124 + LKLog.w { "No target camera found!" }
  125 + return
  126 + } else {
  127 + cameraCapturer.switchCamera(cameraSwitchHandler, targetDeviceId)
  128 + }
  129 + }
  130 +
  131 + /**
  132 + * Restart a track with new options.
  133 + */
80 fun restartTrack(options: LocalVideoTrackOptions = defaultsManager.videoTrackCaptureDefaults.copy()) { 134 fun restartTrack(options: LocalVideoTrackOptions = defaultsManager.videoTrackCaptureDefaults.copy()) {
81 val newTrack = createTrack( 135 val newTrack = createTrack(
82 peerConnectionFactory, 136 peerConnectionFactory,
@@ -183,15 +237,20 @@ constructor( @@ -183,15 +237,20 @@ constructor(
183 ) 237 )
184 } 238 }
185 239
  240 + private fun createCameraEnumerator(context: Context): CameraEnumerator {
  241 + return if (Camera2Enumerator.isSupported(context)) {
  242 + Camera2Enumerator(context)
  243 + } else {
  244 + Camera1Enumerator(true)
  245 + }
  246 + }
  247 +
186 private fun createVideoCapturer( 248 private fun createVideoCapturer(
187 context: Context, 249 context: Context,
188 options: LocalVideoTrackOptions 250 options: LocalVideoTrackOptions
189 ): Pair<VideoCapturer, LocalVideoTrackOptions>? { 251 ): Pair<VideoCapturer, LocalVideoTrackOptions>? {
190 - val pair = if (Camera2Enumerator.isSupported(context)) {  
191 - createCameraCapturer(context, Camera2Enumerator(context), options)  
192 - } else {  
193 - createCameraCapturer(context, Camera1Enumerator(true), options)  
194 - } 252 + val cameraEnumerator = createCameraEnumerator(context)
  253 + val pair = createCameraCapturer(context, cameraEnumerator, options)
195 254
196 if (pair == null) { 255 if (pair == null) {
197 LKLog.d { "Failed to open camera" } 256 LKLog.d { "Failed to open camera" }
@@ -205,31 +264,8 @@ constructor( @@ -205,31 +264,8 @@ constructor(
205 enumerator: CameraEnumerator, 264 enumerator: CameraEnumerator,
206 options: LocalVideoTrackOptions 265 options: LocalVideoTrackOptions
207 ): Pair<VideoCapturer, LocalVideoTrackOptions>? { 266 ): Pair<VideoCapturer, LocalVideoTrackOptions>? {
208 - var targetDeviceName: String? = null  
209 - val targetVideoCapturer: VideoCapturer?  
210 -  
211 - // Prioritize search by deviceId first  
212 - if (options.deviceId != null) {  
213 - targetDeviceName = enumerator.findCamera { deviceName -> deviceName == options.deviceId }  
214 - }  
215 -  
216 - // Search by camera position  
217 - if (targetDeviceName == null && options.position != null) {  
218 - targetDeviceName = enumerator.findCamera { deviceName ->  
219 - enumerator.getCameraPosition(deviceName) == options.position  
220 - }  
221 - }  
222 -  
223 - // Fall back by choosing first available camera.  
224 - if (targetDeviceName == null) {  
225 - targetDeviceName = enumerator.findCamera { true }  
226 - }  
227 -  
228 - if (targetDeviceName == null) {  
229 - return null  
230 - }  
231 -  
232 - targetVideoCapturer = enumerator.createCapturer(targetDeviceName, null) 267 + val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) ?: return null
  268 + val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, null)
233 269
234 // back fill any missing information 270 // back fill any missing information
235 val newOptions = options.copy( 271 val newOptions = options.copy(
@@ -267,7 +303,37 @@ constructor( @@ -267,7 +303,37 @@ constructor(
267 return null 303 return null
268 } 304 }
269 305
270 - fun CameraEnumerator.findCamera(predicate: (deviceName: String) -> Boolean): String? { 306 + private fun CameraEnumerator.findCamera(
  307 + deviceId: String?,
  308 + position: CameraPosition?,
  309 + fallback: Boolean = true
  310 + ): String? {
  311 + var targetDeviceName: String? = null
  312 + // Prioritize search by deviceId first
  313 + if (deviceId != null) {
  314 + targetDeviceName = findCamera { deviceName -> deviceName == deviceId }
  315 + }
  316 +
  317 + // Search by camera position
  318 + if (targetDeviceName == null && position != null) {
  319 + targetDeviceName = findCamera { deviceName ->
  320 + getCameraPosition(deviceName) == position
  321 + }
  322 + }
  323 +
  324 + // Fall back by choosing first available camera.
  325 + if (targetDeviceName == null && fallback) {
  326 + targetDeviceName = findCamera { true }
  327 + }
  328 +
  329 + if (targetDeviceName == null) {
  330 + return null
  331 + }
  332 +
  333 + return targetDeviceName
  334 + }
  335 +
  336 + private fun CameraEnumerator.findCamera(predicate: (deviceName: String) -> Boolean): String? {
271 for (deviceName in deviceNames) { 337 for (deviceName in deviceNames) {
272 if (predicate(deviceName)) { 338 if (predicate(deviceName)) {
273 val videoCapturer = createCapturer(deviceName, null) 339 val videoCapturer = createCapturer(deviceName, null)
@@ -279,7 +345,10 @@ constructor( @@ -279,7 +345,10 @@ constructor(
279 return null 345 return null
280 } 346 }
281 347
282 - fun CameraEnumerator.getCameraPosition(deviceName: String): CameraPosition? { 348 + private fun CameraEnumerator.getCameraPosition(deviceName: String?): CameraPosition? {
  349 + if (deviceName == null) {
  350 + return null
  351 + }
283 if (isBackFacing(deviceName)) { 352 if (isBackFacing(deviceName)) {
284 return CameraPosition.BACK 353 return CameraPosition.BACK
285 } else if (isFrontFacing(deviceName)) { 354 } else if (isFrontFacing(deviceName)) {
@@ -16,7 +16,7 @@ internal interface VideoCapturerWithSize : VideoCapturer { @@ -16,7 +16,7 @@ internal interface VideoCapturerWithSize : VideoCapturer {
16 internal class Camera1CapturerWithSize( 16 internal class Camera1CapturerWithSize(
17 private val capturer: Camera1Capturer, 17 private val capturer: Camera1Capturer,
18 private val deviceName: String? 18 private val deviceName: String?
19 -) : VideoCapturer by capturer, VideoCapturerWithSize { 19 +) : CameraVideoCapturer by capturer, VideoCapturerWithSize {
20 override fun findCaptureFormat(width: Int, height: Int): Size { 20 override fun findCaptureFormat(width: Int, height: Int): Size {
21 val cameraId = Camera1Helper.getCameraId(deviceName) 21 val cameraId = Camera1Helper.getCameraId(deviceName)
22 return Camera1Helper.findClosestCaptureFormat(cameraId, width, height) 22 return Camera1Helper.findClosestCaptureFormat(cameraId, width, height)
@@ -30,7 +30,7 @@ internal class Camera2CapturerWithSize( @@ -30,7 +30,7 @@ internal class Camera2CapturerWithSize(
30 private val capturer: Camera2Capturer, 30 private val capturer: Camera2Capturer,
31 private val cameraManager: CameraManager, 31 private val cameraManager: CameraManager,
32 private val deviceName: String? 32 private val deviceName: String?
33 -) : VideoCapturer by capturer, VideoCapturerWithSize { 33 +) : CameraVideoCapturer by capturer, VideoCapturerWithSize {
34 override fun findCaptureFormat(width: Int, height: Int): Size { 34 override fun findCaptureFormat(width: Int, height: Int): Size {
35 return Camera2Helper.findClosestCaptureFormat(cameraManager, deviceName, width, height) 35 return Camera2Helper.findClosestCaptureFormat(cameraManager, deviceName, width, height)
36 } 36 }
@@ -18,7 +18,10 @@ import io.livekit.android.room.Room @@ -18,7 +18,10 @@ import io.livekit.android.room.Room
18 import io.livekit.android.room.participant.LocalParticipant 18 import io.livekit.android.room.participant.LocalParticipant
19 import io.livekit.android.room.participant.Participant 19 import io.livekit.android.room.participant.Participant
20 import io.livekit.android.room.participant.RemoteParticipant 20 import io.livekit.android.room.participant.RemoteParticipant
21 -import io.livekit.android.room.track.* 21 +import io.livekit.android.room.track.CameraPosition
  22 +import io.livekit.android.room.track.LocalScreencastVideoTrack
  23 +import io.livekit.android.room.track.LocalVideoTrack
  24 +import io.livekit.android.room.track.Track
22 import io.livekit.android.util.flow 25 import io.livekit.android.util.flow
23 import kotlinx.coroutines.flow.* 26 import kotlinx.coroutines.flow.*
24 import kotlinx.coroutines.launch 27 import kotlinx.coroutines.launch
@@ -108,7 +111,9 @@ class CallViewModel( @@ -108,7 +111,9 @@ class CallViewModel(
108 val message = it.data.toString(Charsets.UTF_8) 111 val message = it.data.toString(Charsets.UTF_8)
109 mutableDataReceived.emit("$identity: $message") 112 mutableDataReceived.emit("$identity: $message")
110 } 113 }
111 - else -> {} 114 + else -> {
  115 + Timber.e { "Room event: $it" }
  116 + }
112 } 117 }
113 } 118 }
114 } 119 }
@@ -231,13 +236,13 @@ class CallViewModel( @@ -231,13 +236,13 @@ class CallViewModel(
231 ?.track as? LocalVideoTrack 236 ?.track as? LocalVideoTrack
232 ?: return 237 ?: return
233 238
234 - val newOptions = when (videoTrack.options.position) {  
235 - CameraPosition.FRONT -> LocalVideoTrackOptions(position = CameraPosition.BACK)  
236 - CameraPosition.BACK -> LocalVideoTrackOptions(position = CameraPosition.FRONT)  
237 - else -> LocalVideoTrackOptions() 239 + val newPosition = when (videoTrack.options.position) {
  240 + CameraPosition.FRONT -> CameraPosition.BACK
  241 + CameraPosition.BACK -> CameraPosition.FRONT
  242 + else -> null
238 } 243 }
239 244
240 - videoTrack.restartTrack(newOptions) 245 + videoTrack.switchCamera(position = newPosition)
241 } 246 }
242 247
243 fun dismissError() { 248 fun dismissError() {