davidliu
Committed by GitHub

Expose camera utilities (#289)

* Expose camera utils

* Spotless apply
@@ -19,7 +19,6 @@ package io.livekit.android.room.track @@ -19,7 +19,6 @@ package io.livekit.android.room.track
19 import android.Manifest 19 import android.Manifest
20 import android.content.Context 20 import android.content.Context
21 import android.content.pm.PackageManager 21 import android.content.pm.PackageManager
22 -import android.hardware.camera2.CameraManager  
23 import androidx.core.content.ContextCompat 22 import androidx.core.content.ContextCompat
24 import dagger.assisted.Assisted 23 import dagger.assisted.Assisted
25 import dagger.assisted.AssistedFactory 24 import dagger.assisted.AssistedFactory
@@ -28,6 +27,9 @@ import io.livekit.android.memory.CloseableManager @@ -28,6 +27,9 @@ import io.livekit.android.memory.CloseableManager
28 import io.livekit.android.memory.SurfaceTextureHelperCloser 27 import io.livekit.android.memory.SurfaceTextureHelperCloser
29 import io.livekit.android.room.DefaultsManager 28 import io.livekit.android.room.DefaultsManager
30 import io.livekit.android.room.track.video.* 29 import io.livekit.android.room.track.video.*
  30 +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.getCameraPosition
31 import io.livekit.android.util.FlowObservable 33 import io.livekit.android.util.FlowObservable
32 import io.livekit.android.util.LKLog 34 import io.livekit.android.util.LKLog
33 import io.livekit.android.util.flowDelegate 35 import io.livekit.android.util.flowDelegate
@@ -296,7 +298,7 @@ constructor( @@ -296,7 +298,7 @@ constructor(
296 298
297 val source = peerConnectionFactory.createVideoSource(options.isScreencast) 299 val source = peerConnectionFactory.createVideoSource(options.isScreencast)
298 source.setVideoProcessor(videoProcessor) 300 source.setVideoProcessor(videoProcessor)
299 - val (capturer, newOptions) = createVideoCapturer(context, options) ?: TODO() 301 + val (capturer, newOptions) = CameraCapturerUtils.createCameraCapturer(context, options) ?: TODO()
300 val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext) 302 val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)
301 capturer.initialize( 303 capturer.initialize(
302 surfaceTextureHelper, 304 surfaceTextureHelper,
@@ -320,128 +322,5 @@ constructor( @@ -320,128 +322,5 @@ constructor(
320 322
321 return track 323 return track
322 } 324 }
323 -  
324 - private fun createCameraEnumerator(context: Context): CameraEnumerator {  
325 - return if (Camera2Enumerator.isSupported(context)) {  
326 - Camera2Enumerator(context)  
327 - } else {  
328 - Camera1Enumerator(true)  
329 - }  
330 - }  
331 -  
332 - private fun createVideoCapturer(  
333 - context: Context,  
334 - options: LocalVideoTrackOptions  
335 - ): Pair<VideoCapturer, LocalVideoTrackOptions>? {  
336 - val cameraEnumerator = createCameraEnumerator(context)  
337 - val pair = createCameraCapturer(context, cameraEnumerator, options)  
338 -  
339 - if (pair == null) {  
340 - LKLog.d { "Failed to open camera" }  
341 - return null  
342 - }  
343 - return pair  
344 - }  
345 -  
346 - private fun createCameraCapturer(  
347 - context: Context,  
348 - enumerator: CameraEnumerator,  
349 - options: LocalVideoTrackOptions  
350 - ): Pair<VideoCapturer, LocalVideoTrackOptions>? {  
351 - val cameraEventsDispatchHandler = CameraEventsDispatchHandler()  
352 - val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) ?: return null  
353 - val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, cameraEventsDispatchHandler)  
354 -  
355 - // back fill any missing information  
356 - val newOptions = options.copy(  
357 - deviceId = targetDeviceName,  
358 - position = enumerator.getCameraPosition(targetDeviceName)  
359 - )  
360 - if (targetVideoCapturer is Camera1Capturer) {  
361 - // Cache supported capture formats ahead of time to avoid future camera locks.  
362 - Camera1Helper.getSupportedFormats(Camera1Helper.getCameraId(newOptions.deviceId))  
363 - return Pair(  
364 - Camera1CapturerWithSize(  
365 - targetVideoCapturer,  
366 - targetDeviceName,  
367 - cameraEventsDispatchHandler  
368 - ),  
369 - newOptions  
370 - )  
371 - }  
372 -  
373 - if (targetVideoCapturer is Camera2Capturer) {  
374 - return Pair(  
375 - Camera2CapturerWithSize(  
376 - targetVideoCapturer,  
377 - context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,  
378 - targetDeviceName,  
379 - cameraEventsDispatchHandler  
380 - ),  
381 - newOptions  
382 - )  
383 - }  
384 -  
385 - LKLog.w { "unknown CameraCapturer class: ${targetVideoCapturer.javaClass.canonicalName}. Reported dimensions may be inaccurate." }  
386 - if (targetVideoCapturer != null) {  
387 - return Pair(  
388 - targetVideoCapturer,  
389 - newOptions  
390 - )  
391 - }  
392 -  
393 - return null  
394 - }  
395 -  
396 - private fun CameraEnumerator.findCamera(  
397 - deviceId: String?,  
398 - position: CameraPosition?,  
399 - fallback: Boolean = true  
400 - ): String? {  
401 - var targetDeviceName: String? = null  
402 - // Prioritize search by deviceId first  
403 - if (deviceId != null) {  
404 - targetDeviceName = findCamera { deviceName -> deviceName == deviceId }  
405 - }  
406 -  
407 - // Search by camera position  
408 - if (targetDeviceName == null && position != null) {  
409 - targetDeviceName = findCamera { deviceName ->  
410 - getCameraPosition(deviceName) == position  
411 - }  
412 - }  
413 -  
414 - // Fall back by choosing first available camera.  
415 - if (targetDeviceName == null && fallback) {  
416 - targetDeviceName = findCamera { true }  
417 - }  
418 -  
419 - if (targetDeviceName == null) {  
420 - return null  
421 - }  
422 -  
423 - return targetDeviceName  
424 - }  
425 -  
426 - private fun CameraEnumerator.findCamera(predicate: (deviceName: String) -> Boolean): String? {  
427 - for (deviceName in deviceNames) {  
428 - if (predicate(deviceName)) {  
429 - return deviceName  
430 - }  
431 - }  
432 - return null  
433 - }  
434 -  
435 - private fun CameraEnumerator.getCameraPosition(deviceName: String?): CameraPosition? {  
436 - if (deviceName == null) {  
437 - return null  
438 - }  
439 - if (isBackFacing(deviceName)) {  
440 - return CameraPosition.BACK  
441 - } else if (isFrontFacing(deviceName)) {  
442 - return CameraPosition.FRONT  
443 - }  
444 - return null  
445 - }  
446 } 325 }
447 } 326 }
  1 +/*
  2 + * Copyright 2023 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@file:Suppress("MemberVisibilityCanBePrivate", "unused")
  18 +
  19 +package io.livekit.android.room.track.video
  20 +
  21 +import android.content.Context
  22 +import android.hardware.camera2.CameraManager
  23 +import io.livekit.android.room.track.CameraPosition
  24 +import io.livekit.android.room.track.LocalVideoTrackOptions
  25 +import io.livekit.android.util.LKLog
  26 +import org.webrtc.Camera1Capturer
  27 +import org.webrtc.Camera1Enumerator
  28 +import org.webrtc.Camera1Helper
  29 +import org.webrtc.Camera2Capturer
  30 +import org.webrtc.Camera2Enumerator
  31 +import org.webrtc.CameraEnumerator
  32 +import org.webrtc.VideoCapturer
  33 +
  34 +object CameraCapturerUtils {
  35 +
  36 + /**
  37 + * Create a CameraEnumerator based on platform capabilities.
  38 + *
  39 + * If available, creates an enumerator that uses Camera2. If not,
  40 + * a Camera1 enumerator is created.
  41 + */
  42 + fun createCameraEnumerator(context: Context): CameraEnumerator {
  43 + return if (Camera2Enumerator.isSupported(context)) {
  44 + Camera2Enumerator(context)
  45 + } else {
  46 + Camera1Enumerator(true)
  47 + }
  48 + }
  49 +
  50 + /**
  51 + * Creates a Camera capturer.
  52 + */
  53 + fun createCameraCapturer(
  54 + context: Context,
  55 + options: LocalVideoTrackOptions,
  56 + ): Pair<VideoCapturer, LocalVideoTrackOptions>? {
  57 + val cameraEnumerator = createCameraEnumerator(context)
  58 + val pair = createCameraCapturer(context, cameraEnumerator, options)
  59 +
  60 + if (pair == null) {
  61 + LKLog.d { "Failed to open camera" }
  62 + return null
  63 + }
  64 + return pair
  65 + }
  66 +
  67 + private fun createCameraCapturer(
  68 + context: Context,
  69 + enumerator: CameraEnumerator,
  70 + options: LocalVideoTrackOptions,
  71 + ): Pair<VideoCapturer, LocalVideoTrackOptions>? {
  72 + val cameraEventsDispatchHandler = CameraEventsDispatchHandler()
  73 + val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) ?: return null
  74 + val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, cameraEventsDispatchHandler)
  75 +
  76 + // back fill any missing information
  77 + val newOptions = options.copy(
  78 + deviceId = targetDeviceName,
  79 + position = enumerator.getCameraPosition(targetDeviceName),
  80 + )
  81 + if (targetVideoCapturer is Camera1Capturer) {
  82 + // Cache supported capture formats ahead of time to avoid future camera locks.
  83 + Camera1Helper.getSupportedFormats(Camera1Helper.getCameraId(newOptions.deviceId))
  84 + return Pair(
  85 + Camera1CapturerWithSize(
  86 + targetVideoCapturer,
  87 + targetDeviceName,
  88 + cameraEventsDispatchHandler,
  89 + ),
  90 + newOptions,
  91 + )
  92 + }
  93 +
  94 + if (targetVideoCapturer is Camera2Capturer) {
  95 + return Pair(
  96 + Camera2CapturerWithSize(
  97 + targetVideoCapturer,
  98 + context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,
  99 + targetDeviceName,
  100 + cameraEventsDispatchHandler,
  101 + ),
  102 + newOptions,
  103 + )
  104 + }
  105 +
  106 + LKLog.w { "unknown CameraCapturer class: ${targetVideoCapturer.javaClass.canonicalName}. Reported dimensions may be inaccurate." }
  107 + if (targetVideoCapturer != null) {
  108 + return Pair(
  109 + targetVideoCapturer,
  110 + newOptions,
  111 + )
  112 + }
  113 +
  114 + return null
  115 + }
  116 +
  117 + /**
  118 + * Finds the device id of first available camera based on the criteria given. Returns null if no camera matches the criteria.
  119 + *
  120 + * @param deviceId an id of a camera. Available device ids can be found through [CameraEnumerator.getDeviceNames]. If null, device id search is skipped. Defaults to null.
  121 + * @param position the position of a camera. If null, search based on camera position is skipped. Defaults to null.
  122 + * @param fallback if true, when no camera is found by device id/position search, the first available camera on the list will be returned.
  123 + */
  124 + fun CameraEnumerator.findCamera(
  125 + deviceId: String? = null,
  126 + position: CameraPosition? = null,
  127 + fallback: Boolean = true,
  128 + ): String? {
  129 + var targetDeviceName: String? = null
  130 + // Prioritize search by deviceId first
  131 + if (deviceId != null) {
  132 + targetDeviceName = findCamera { deviceName -> deviceName == deviceId }
  133 + }
  134 +
  135 + // Search by camera position
  136 + if (targetDeviceName == null && position != null) {
  137 + targetDeviceName = findCamera { deviceName ->
  138 + getCameraPosition(deviceName) == position
  139 + }
  140 + }
  141 +
  142 + // Fall back by choosing first available camera.
  143 + if (targetDeviceName == null && fallback) {
  144 + targetDeviceName = findCamera { true }
  145 + }
  146 +
  147 + if (targetDeviceName == null) {
  148 + return null
  149 + }
  150 +
  151 + return targetDeviceName
  152 + }
  153 +
  154 + /**
  155 + * Finds the device id of a camera that matches the [predicate].
  156 + */
  157 + fun CameraEnumerator.findCamera(predicate: (deviceName: String) -> Boolean): String? {
  158 + for (deviceName in deviceNames) {
  159 + if (predicate(deviceName)) {
  160 + return deviceName
  161 + }
  162 + }
  163 + return null
  164 + }
  165 +
  166 + /**
  167 + * Returns the camera position of a camera, or null if neither front or back facing (e.g. external camera).
  168 + */
  169 + fun CameraEnumerator.getCameraPosition(deviceName: String?): CameraPosition? {
  170 + if (deviceName == null) {
  171 + return null
  172 + }
  173 + if (isBackFacing(deviceName)) {
  174 + return CameraPosition.BACK
  175 + } else if (isFrontFacing(deviceName)) {
  176 + return CameraPosition.FRONT
  177 + }
  178 + return null
  179 + }
  180 +}