Committed by
GitHub
Switch camera api for changing cameras directly (#149)
restartTrack has some issues on specific devices, causing camera errors.
正在显示
3 个修改的文件
包含
115 行增加
和
41 行删除
| @@ -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() { |
-
请 注册 或 登录 后发表评论