jfilo-ebay
Committed by GitHub

CameraX integration (#421)

* Allow registration of external cameras implementations

* CameraX sample integration

* create livekit-android-camerax module

* spotless

---------

Co-authored-by: davidliu <davidliu@deviange.net>
@@ -3,9 +3,10 @@ webrtc = "114.5735.09" @@ -3,9 +3,10 @@ webrtc = "114.5735.09"
3 3
4 androidJainSipRi = "1.3.0-91" 4 androidJainSipRi = "1.3.0-91"
5 androidx-activity = "1.9.0" 5 androidx-activity = "1.9.0"
  6 +androidx-camera = "1.3.3"
6 androidx-core = "1.13.1" 7 androidx-core = "1.13.1"
7 androidx-fragment = "1.5.1" 8 androidx-fragment = "1.5.1"
8 -androidx-lifecycle = "2.5.1" 9 +androidx-lifecycle = "2.8.0"
9 audioswitch = "89582c47c9a04c62f90aa5e57251af4800a62c9a" 10 audioswitch = "89582c47c9a04c62f90aa5e57251af4800a62c9a"
10 autoService = '1.0.1' 11 autoService = '1.0.1'
11 coroutines = "1.6.0" 12 coroutines = "1.6.0"
@@ -28,6 +29,12 @@ viewpager2 = "1.0.0" @@ -28,6 +29,12 @@ viewpager2 = "1.0.0"
28 [libraries] 29 [libraries]
29 android-jain-sip-ri = { module = "javax.sip:android-jain-sip-ri", version.ref = "androidJainSipRi" } 30 android-jain-sip-ri = { module = "javax.sip:android-jain-sip-ri", version.ref = "androidJainSipRi" }
30 androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } 31 androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
  32 +androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
  33 +androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
  34 +androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "androidx-camera" }
  35 +androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
  36 +androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "androidx-camera" }
  37 +androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
31 androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } 38 androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }
32 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } 39 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
33 androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } 40 androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" }
@@ -38,8 +45,9 @@ dagger-lib = { module = "com.google.dagger:dagger", version.ref = "dagger" } @@ -38,8 +45,9 @@ dagger-lib = { module = "com.google.dagger:dagger", version.ref = "dagger" }
38 dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } 45 dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
39 groupie = { module = "com.github.lisawray.groupie:groupie", version.ref = "groupie" } 46 groupie = { module = "com.github.lisawray.groupie:groupie", version.ref = "groupie" }
40 groupie-viewbinding = { module = "com.github.lisawray.groupie:groupie-viewbinding", version.ref = "groupie" } 47 groupie-viewbinding = { module = "com.github.lisawray.groupie:groupie-viewbinding", version.ref = "groupie" }
41 -androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }  
42 androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" } 48 androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" }
  49 +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
  50 +androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" }
43 androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } 51 androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
44 kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 52 kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
45 leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" } 53 leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" }
  1 +plugins {
  2 + id "org.jetbrains.dokka"
  3 + id 'com.android.library'
  4 + id 'kotlin-android'
  5 + id 'kotlin-kapt'
  6 +}
  7 +
  8 +android {
  9 + namespace 'io.livekit.android.camerax'
  10 + compileSdkVersion androidSdk.compileVersion
  11 +
  12 + defaultConfig {
  13 + minSdkVersion androidSdk.minVersion
  14 + targetSdkVersion androidSdk.targetVersion
  15 +
  16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  17 + consumerProguardFiles "consumer-rules.pro"
  18 + }
  19 +
  20 + buildTypes {
  21 + release {
  22 + minifyEnabled false
  23 + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
  24 + }
  25 + }
  26 + compileOptions {
  27 + sourceCompatibility java_version
  28 + targetCompatibility java_version
  29 + }
  30 + kotlinOptions {
  31 + freeCompilerArgs = ["-Xinline-classes", "-opt-in=kotlin.RequiresOptIn"]
  32 + jvmTarget = java_version
  33 + }
  34 + testOptions {
  35 + unitTests {
  36 + includeAndroidResources = true
  37 + }
  38 + }
  39 +}
  40 +
  41 +dokkaHtml {
  42 + moduleName.set("livekit-android-test")
  43 + dokkaSourceSets {
  44 + configureEach {
  45 + skipEmptyPackages.set(true)
  46 + includeNonPublic.set(false)
  47 + includes.from("module.md")
  48 + displayName.set("LiveKit CameraX")
  49 + sourceLink {
  50 + localDirectory.set(file("src/main/java"))
  51 +
  52 + // URL showing where the source code can be accessed through the web browser
  53 + remoteUrl.set(new URL(
  54 + "https://github.com/livekit/client-sdk-android/tree/master/livekit-android-camerax/src/main/java"))
  55 + // Suffix which is used to append the line number to the URL. Use #L for GitHub
  56 + remoteLineSuffix.set("#L")
  57 + }
  58 +
  59 + perPackageOption {
  60 + matchingRegex.set(".*\\.dagger.*")
  61 + suppress.set(true)
  62 + }
  63 +
  64 + perPackageOption {
  65 + matchingRegex.set(".*\\.util.*")
  66 + suppress.set(true)
  67 + }
  68 + }
  69 + }
  70 +}
  71 +
  72 +dependencies {
  73 +
  74 + implementation(project(":livekit-android-sdk"))
  75 + implementation libs.timber
  76 + implementation libs.coroutines.lib
  77 + implementation libs.androidx.annotation
  78 +
  79 + api libs.androidx.camera.core
  80 + api libs.androidx.camera.camera2
  81 + api libs.androidx.camera.lifecycle
  82 +
  83 + testImplementation libs.junit
  84 + testImplementation libs.robolectric
  85 +
  86 + androidTestImplementation libs.androidx.test.junit
  87 + androidTestImplementation libs.espresso
  88 +}
  89 +tasks.withType(Test).configureEach {
  90 + systemProperty "robolectric.logging.enabled", true
  91 +}
  92 +
  93 +apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
  94 +
  95 +afterEvaluate {
  96 + publishing {
  97 + publications {
  98 + // Creates a Maven publication called "release".
  99 + release(MavenPublication) {
  100 + // Applies the component for the release build variant.
  101 + from components.release
  102 +
  103 + // You can then customize attributes of the publication as shown below.
  104 + groupId = GROUP
  105 + artifactId = POM_ARTIFACT_ID
  106 + version = VERSION_NAME
  107 + }
  108 + }
  109 + }
  110 +}
  1 +POM_NAME=CameraX Support for LiveKit Android SDK
  2 +POM_ARTIFACT_ID=livekit-android-camerax
  3 +POM_PACKAGING=aar
  1 +# Add project specific ProGuard rules here.
  2 +# You can control the set of applied configuration files using the
  3 +# proguardFiles setting in build.gradle.
  4 +#
  5 +# For more details, see
  6 +# http://developer.android.com/guide/developing/tools/proguard.html
  7 +
  8 +# If your project uses WebView with JS, uncomment the following
  9 +# and specify the fully qualified class name to the JavaScript interface
  10 +# class:
  11 +#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
  12 +# public *;
  13 +#}
  14 +
  15 +# Uncomment this to preserve the line number information for
  16 +# debugging stack traces.
  17 +#-keepattributes SourceFile,LineNumberTable
  18 +
  19 +# If you keep the line number information, uncomment this to
  20 +# hide the original source file name.
  21 +#-renamesourcefileattribute SourceFile
  1 +<?xml version="1.0" encoding="utf-8"?>
  2 +<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  3 +
  4 +</manifest>
  1 +/*
  2 + * Copyright 2024 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 +package livekit.org.webrtc
  18 +
  19 +import android.content.Context
  20 +import android.hardware.camera2.CameraManager
  21 +import androidx.camera.camera2.interop.ExperimentalCamera2Interop
  22 +import androidx.lifecycle.LifecycleOwner
  23 +import io.livekit.android.room.track.video.CameraCapturerWithSize
  24 +import io.livekit.android.room.track.video.CameraEventsDispatchHandler
  25 +
  26 +@ExperimentalCamera2Interop
  27 +internal class CameraXCapturer(
  28 + context: Context,
  29 + private val lifecycleOwner: LifecycleOwner,
  30 + cameraName: String?,
  31 + eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
  32 +) : CameraCapturer(cameraName, eventsHandler, Camera2Enumerator(context)) {
  33 +
  34 + var cameraControlListener: CameraXSession.CameraControlListener? = null
  35 +
  36 + override fun createCameraSession(
  37 + createSessionCallback: CameraSession.CreateSessionCallback,
  38 + events: CameraSession.Events,
  39 + applicationContext: Context,
  40 + surfaceTextureHelper: SurfaceTextureHelper,
  41 + cameraName: String,
  42 + width: Int,
  43 + height: Int,
  44 + framerate: Int,
  45 + ) {
  46 + CameraXSession(
  47 + createSessionCallback,
  48 + events,
  49 + applicationContext,
  50 + lifecycleOwner,
  51 + surfaceTextureHelper,
  52 + cameraName,
  53 + width,
  54 + height,
  55 + framerate,
  56 + cameraControlListener,
  57 + )
  58 + }
  59 +}
  60 +
  61 +@ExperimentalCamera2Interop
  62 +internal class CameraXCapturerWithSize(
  63 + private val capturer: CameraXCapturer,
  64 + private val cameraManager: CameraManager,
  65 + private val deviceName: String?,
  66 + cameraEventsDispatchHandler: CameraEventsDispatchHandler,
  67 +) : CameraCapturerWithSize(cameraEventsDispatchHandler), CameraVideoCapturer by capturer {
  68 + override fun findCaptureFormat(width: Int, height: Int): Size {
  69 + return CameraXHelper.findClosestCaptureFormat(cameraManager, deviceName, width, height)
  70 + }
  71 +}
  1 +/*
  2 + * Copyright 2024 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 +package livekit.org.webrtc
  18 +
  19 +import android.content.Context
  20 +import android.graphics.Rect
  21 +import android.graphics.SurfaceTexture
  22 +import android.hardware.camera2.CameraCharacteristics
  23 +import android.os.Build
  24 +import android.os.Build.VERSION
  25 +import androidx.camera.camera2.interop.Camera2CameraInfo
  26 +import androidx.camera.camera2.interop.ExperimentalCamera2Interop
  27 +import androidx.lifecycle.LifecycleOwner
  28 +
  29 +@ExperimentalCamera2Interop
  30 +class CameraXEnumerator(
  31 + context: Context,
  32 + private val lifecycleOwner: LifecycleOwner,
  33 +) : Camera2Enumerator(context) {
  34 +
  35 + override fun createCapturer(
  36 + deviceName: String?,
  37 + eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
  38 + ): CameraVideoCapturer {
  39 + return CameraXCapturer(context, lifecycleOwner, deviceName, eventsHandler)
  40 + }
  41 +
  42 + companion object {
  43 + fun getSupportedSizes(camera: Camera2CameraInfo): List<Size> {
  44 + val streamMap = camera.getCameraCharacteristic(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
  45 + val supportLevel = camera.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
  46 + val nativeSizes = streamMap!!.getOutputSizes(SurfaceTexture::class.java)
  47 + val sizes = convertSizes(nativeSizes)
  48 + val activeArraySize: Rect? = camera.getCameraCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
  49 + return if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1 &&
  50 + supportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY &&
  51 + activeArraySize != null
  52 + ) {
  53 + val filteredSizes = ArrayList<Size>()
  54 + for (size in sizes) {
  55 + if (activeArraySize.width() * size.height == activeArraySize.height() * size.width) {
  56 + filteredSizes.add(size)
  57 + }
  58 + }
  59 + filteredSizes
  60 + } else {
  61 + sizes
  62 + }
  63 + }
  64 +
  65 + private fun convertSizes(cameraSizes: Array<android.util.Size>): List<Size> {
  66 + val sizes: MutableList<Size> = ArrayList()
  67 + for (size in cameraSizes) {
  68 + sizes.add(Size(size.width, size.height))
  69 + }
  70 + return sizes
  71 + }
  72 + }
  73 +}
  1 +/*
  2 + * Copyright 2023-2024 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 +package livekit.org.webrtc
  18 +
  19 +import android.content.Context
  20 +import android.hardware.camera2.CameraManager
  21 +import androidx.camera.camera2.interop.ExperimentalCamera2Interop
  22 +import androidx.lifecycle.LifecycleOwner
  23 +import io.livekit.android.room.track.LocalVideoTrackOptions
  24 +import io.livekit.android.room.track.video.CameraCapturerUtils
  25 +import io.livekit.android.room.track.video.CameraCapturerUtils.findCamera
  26 +import io.livekit.android.room.track.video.CameraEventsDispatchHandler
  27 +
  28 +class CameraXHelper {
  29 + companion object {
  30 +
  31 + @ExperimentalCamera2Interop
  32 + fun getCameraProvider(
  33 + lifecycleOwner: LifecycleOwner,
  34 + controlListener: CameraXSession.CameraControlListener?,
  35 + ) = object : CameraCapturerUtils.CameraProvider {
  36 +
  37 + private var enumerator: CameraXEnumerator? = null
  38 +
  39 + override val cameraVersion = 3
  40 +
  41 + override fun provideEnumerator(context: Context): CameraXEnumerator =
  42 + enumerator ?: CameraXEnumerator(context, lifecycleOwner).also {
  43 + enumerator = it
  44 + }
  45 +
  46 + override fun provideCapturer(
  47 + context: Context,
  48 + options: LocalVideoTrackOptions,
  49 + eventsHandler: CameraEventsDispatchHandler,
  50 + ): VideoCapturer {
  51 + val enumerator = provideEnumerator(context)
  52 + val targetDeviceName = enumerator.findCamera(options.deviceId, options.position)
  53 + val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) as CameraXCapturer
  54 + controlListener?.let {
  55 + targetVideoCapturer.cameraControlListener = it
  56 + }
  57 + return CameraXCapturerWithSize(
  58 + targetVideoCapturer,
  59 + context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,
  60 + targetDeviceName,
  61 + eventsHandler,
  62 + )
  63 + }
  64 +
  65 + override fun isSupported(context: Context) = Camera2Enumerator.isSupported(context)
  66 + }
  67 +
  68 + private fun getSupportedFormats(
  69 + cameraManager: CameraManager,
  70 + cameraId: String?,
  71 + ): List<CameraEnumerationAndroid.CaptureFormat>? =
  72 + Camera2Enumerator.getSupportedFormats(cameraManager, cameraId)
  73 +
  74 + fun findClosestCaptureFormat(
  75 + cameraManager: CameraManager,
  76 + cameraId: String?,
  77 + width: Int,
  78 + height: Int,
  79 + ): Size {
  80 + val sizes = getSupportedFormats(cameraManager, cameraId)
  81 + ?.map { Size(it.width, it.height) }
  82 + ?: emptyList()
  83 + return CameraEnumerationAndroid.getClosestSupportedSize(sizes, width, height)
  84 + }
  85 + }
  86 +}
  1 +/*
  2 + * Copyright 2024 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 +package livekit.org.webrtc
  18 +
  19 +import android.content.Context
  20 +import android.hardware.camera2.CameraCharacteristics
  21 +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_ON
  24 +import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_OFF
  25 +import android.hardware.camera2.CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON
  26 +import android.hardware.camera2.CaptureRequest
  27 +import android.util.Range
  28 +import android.util.Size
  29 +import android.view.Surface
  30 +import androidx.camera.camera2.interop.Camera2CameraInfo
  31 +import androidx.camera.camera2.interop.Camera2Interop
  32 +import androidx.camera.core.Camera
  33 +import androidx.camera.core.CameraControl
  34 +import androidx.camera.core.CameraSelector
  35 +import androidx.camera.core.ImageAnalysis
  36 +import androidx.camera.core.Preview
  37 +import androidx.camera.core.Preview.SurfaceProvider
  38 +import androidx.camera.lifecycle.ProcessCameraProvider
  39 +import androidx.core.content.ContextCompat
  40 +import androidx.lifecycle.LifecycleOwner
  41 +import livekit.org.webrtc.CameraEnumerationAndroid.CaptureFormat
  42 +import java.util.concurrent.Executor
  43 +import java.util.concurrent.TimeUnit
  44 +
  45 +@androidx.camera.camera2.interop.ExperimentalCamera2Interop
  46 +class CameraXSession
  47 +internal constructor(
  48 + private val sessionCallback: CameraSession.CreateSessionCallback,
  49 + private val events: CameraSession.Events,
  50 + private val context: Context,
  51 + private val lifecycleOwner: LifecycleOwner,
  52 + private val surfaceTextureHelper: SurfaceTextureHelper,
  53 + private val cameraId: String,
  54 + private val width: Int,
  55 + private val height: Int,
  56 + private val frameRate: Int,
  57 + private val cameraControlListener: CameraControlListener? = null,
  58 +) : CameraSession {
  59 +
  60 + private var state = SessionState.RUNNING
  61 + private var cameraThreadHandler = surfaceTextureHelper.handler
  62 + private lateinit var cameraProvider: ProcessCameraProvider
  63 + private lateinit var surfaceProvider: SurfaceProvider
  64 + private var camera: Camera? = null
  65 + private var surface: Surface? = null
  66 + private var cameraOrientation: Int = 0
  67 + private var isCameraFrontFacing: Boolean = true
  68 + private var firstFrameReported = false
  69 + private val constructionTimeNs = System.nanoTime()
  70 + private var fpsUnitFactor = 1
  71 + private var captureFormat: CaptureFormat? = null
  72 + private var stabilizationMode = StabilizationMode.NONE
  73 + private var surfaceTextureListener = { frame: VideoFrame ->
  74 + checkIsOnCameraThread()
  75 + if (state != SessionState.RUNNING) {
  76 + Logging.d(TAG, "Texture frame captured but camera is no longer running.")
  77 + } else {
  78 + if (!firstFrameReported) {
  79 + firstFrameReported = true
  80 + val startTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - constructionTimeNs).toInt()
  81 + cameraXStartTimeMsHistogram.addSample(startTimeMs)
  82 + }
  83 + // Undo the mirror that the OS "helps" us with.
  84 + // http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
  85 + // Also, undo camera orientation, we report it as rotation instead.
  86 + val modifiedFrame = VideoFrame(
  87 + CameraSession.createTextureBufferWithModifiedTransformMatrix(
  88 + frame.buffer as TextureBufferImpl,
  89 + isCameraFrontFacing,
  90 + -cameraOrientation,
  91 + ),
  92 + getFrameOrientation(),
  93 + frame.timestampNs,
  94 + )
  95 + events.onFrameCaptured(this@CameraXSession, modifiedFrame)
  96 + modifiedFrame.release()
  97 + }
  98 + }
  99 +
  100 + init {
  101 + cameraThreadHandler.post {
  102 + start()
  103 + }
  104 + }
  105 +
  106 + private fun start() {
  107 + checkIsOnCameraThread()
  108 + Logging.d(TAG, "start")
  109 + surfaceTextureHelper.startListening(surfaceTextureListener)
  110 + openCamera()
  111 + }
  112 +
  113 + override fun stop() {
  114 + Logging.d(TAG, "Stop cameraX session on camera $cameraId")
  115 + checkIsOnCameraThread()
  116 + if (state != SessionState.STOPPED) {
  117 + val stopStartTime = System.nanoTime()
  118 + state = SessionState.STOPPED
  119 + stopInternal()
  120 + val stopTimeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - stopStartTime).toInt()
  121 + cameraXStopTimeMsHistogram.addSample(stopTimeMs)
  122 + }
  123 + }
  124 +
  125 + private fun openCamera() {
  126 + checkIsOnCameraThread()
  127 + Logging.d(TAG, "Opening camera $cameraId")
  128 + events.onCameraOpening()
  129 + val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
  130 + val helperExecutor = Executor { command ->
  131 + surfaceTextureHelper.handler.let {
  132 + if (it.looper.thread.isAlive) {
  133 + it.post(command)
  134 + }
  135 + }
  136 + }
  137 + cameraProviderFuture.addListener(
  138 + {
  139 + // Used to bind the lifecycle of cameras to the lifecycle owner
  140 + cameraProvider = cameraProviderFuture.get()
  141 + obtainCameraConfiguration()
  142 +
  143 + surfaceTextureHelper.setTextureSize(captureFormat?.width ?: width, captureFormat?.height ?: height)
  144 +
  145 + surface = Surface(surfaceTextureHelper.surfaceTexture)
  146 + surfaceProvider = SurfaceProvider { request ->
  147 + request.provideSurface(surface!!, helperExecutor) { }
  148 + }
  149 +
  150 + // Set image analysis - camera params
  151 + val imageAnalysis = setImageAnalysis()
  152 +
  153 + // Select camera by ID
  154 + val cameraSelector = CameraSelector.Builder()
  155 + .addCameraFilter { cameraInfo -> cameraInfo.filter { Camera2CameraInfo.from(it).cameraId == cameraId } }
  156 + .build()
  157 +
  158 + try {
  159 + ContextCompat.getMainExecutor(context).execute {
  160 + // Preview
  161 + val preview = Preview.Builder()
  162 + .build()
  163 + .also {
  164 + it.setSurfaceProvider(surfaceProvider)
  165 + }
  166 +
  167 + // Unbind use cases before rebinding
  168 + cameraProvider.unbindAll()
  169 +
  170 + // Bind use cases to camera
  171 + camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview).apply {
  172 + cameraControlListener?.onCameraControlAvailable(this.cameraControl)
  173 + }
  174 + }
  175 + sessionCallback.onDone(this@CameraXSession)
  176 + } catch (e: Exception) {
  177 + reportError("Failed to open camera: $e")
  178 + }
  179 + },
  180 + helperExecutor,
  181 + )
  182 + }
  183 +
  184 + private fun setImageAnalysis() = ImageAnalysis.Builder()
  185 + .setTargetResolution(Size(captureFormat?.width ?: width, captureFormat?.height ?: height))
  186 + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).apply {
  187 + val cameraExtender = Camera2Interop.Extender(this)
  188 + captureFormat?.let { captureFormat ->
  189 + cameraExtender.setCaptureRequestOption(
  190 + CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
  191 + Range(
  192 + captureFormat.framerate.min / fpsUnitFactor,
  193 + captureFormat.framerate.max / fpsUnitFactor,
  194 + ),
  195 + )
  196 + cameraExtender.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
  197 + cameraExtender.setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, false)
  198 + when (stabilizationMode) {
  199 + StabilizationMode.OPTICAL -> {
  200 + cameraExtender.setCaptureRequestOption(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, LENS_OPTICAL_STABILIZATION_MODE_ON)
  201 + cameraExtender.setCaptureRequestOption(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_OFF)
  202 + }
  203 +
  204 + StabilizationMode.VIDEO -> {
  205 + cameraExtender.setCaptureRequestOption(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, CONTROL_VIDEO_STABILIZATION_MODE_ON)
  206 + cameraExtender.setCaptureRequestOption(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, LENS_OPTICAL_STABILIZATION_MODE_OFF)
  207 + }
  208 +
  209 + else -> Unit
  210 + }
  211 + }
  212 + }
  213 + .build()
  214 +
  215 + private fun stopInternal() {
  216 + Logging.d(TAG, "Stop internal")
  217 + checkIsOnCameraThread()
  218 + surfaceTextureHelper.stopListening()
  219 +
  220 + if (surface != null) {
  221 + surface!!.release()
  222 + surface = null
  223 + }
  224 +
  225 + ContextCompat.getMainExecutor(context).execute {
  226 + cameraProvider.unbindAll()
  227 + }
  228 + Logging.d(TAG, "Stop done")
  229 + }
  230 +
  231 + private fun reportError(error: String) {
  232 + checkIsOnCameraThread()
  233 + Logging.e(TAG, "Error: $error")
  234 + val startFailure = camera == null && state != SessionState.STOPPED
  235 + state = SessionState.STOPPED
  236 + stopInternal()
  237 + if (startFailure) {
  238 + sessionCallback.onFailure(CameraSession.FailureType.ERROR, error)
  239 + } else {
  240 + events.onCameraError(this, error)
  241 + }
  242 + }
  243 +
  244 + private fun obtainCameraConfiguration() {
  245 + val camera = cameraProvider.availableCameraInfos.map { Camera2CameraInfo.from(it) }.first { it.cameraId == cameraId }
  246 +
  247 + cameraOrientation = camera.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION) ?: -1
  248 + isCameraFrontFacing = camera.getCameraCharacteristic(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT
  249 +
  250 + findCaptureFormat(camera)
  251 + findStabilizationMode(camera)
  252 + }
  253 +
  254 + private fun findCaptureFormat(camera: Camera2CameraInfo) {
  255 + val fpsRanges = camera.getCameraCharacteristic(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
  256 + fpsUnitFactor = Camera2Enumerator.getFpsUnitFactor(fpsRanges)
  257 + val framerateRanges = Camera2Enumerator.convertFramerates(fpsRanges, fpsUnitFactor)
  258 + val sizes = CameraXEnumerator.getSupportedSizes(camera)
  259 + Logging.d(TAG, "Available preview sizes: $sizes")
  260 + Logging.d(TAG, "Available fps ranges: $framerateRanges")
  261 + if (framerateRanges.isEmpty() || sizes.isEmpty()) {
  262 + reportError("No supported capture formats.")
  263 + return
  264 + }
  265 + val bestFpsRange = CameraEnumerationAndroid.getClosestSupportedFramerateRange(framerateRanges, frameRate)
  266 + val bestSize = CameraEnumerationAndroid.getClosestSupportedSize(sizes, width, height)
  267 + CameraEnumerationAndroid.reportCameraResolution(cameraXResolutionHistogram, bestSize)
  268 + captureFormat = CaptureFormat(bestSize.width, bestSize.height, bestFpsRange)
  269 + Logging.d(TAG, "Using capture format: $captureFormat")
  270 + }
  271 +
  272 + private fun findStabilizationMode(camera: Camera2CameraInfo) {
  273 + val availableOpticalStabilization: IntArray? = camera.getCameraCharacteristic(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION)
  274 + if (availableOpticalStabilization?.contains(LENS_OPTICAL_STABILIZATION_MODE_ON) == true) {
  275 + stabilizationMode = StabilizationMode.OPTICAL
  276 + } else {
  277 + val availableVideoStabilization: IntArray? = camera.getCameraCharacteristic(CameraCharacteristics.LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION)
  278 + if (availableVideoStabilization?.contains(CONTROL_VIDEO_STABILIZATION_MODE_ON) == true) {
  279 + stabilizationMode = StabilizationMode.VIDEO
  280 + }
  281 + }
  282 + }
  283 +
  284 + private fun checkIsOnCameraThread() {
  285 + check(Thread.currentThread() === cameraThreadHandler.looper.thread) { "Wrong thread" }
  286 + }
  287 +
  288 + private fun getFrameOrientation(): Int {
  289 + var rotation = CameraSession.getDeviceOrientation(context)
  290 + if (!isCameraFrontFacing) {
  291 + rotation = 360 - rotation
  292 + }
  293 + return (cameraOrientation + rotation) % 360
  294 + }
  295 +
  296 + companion object {
  297 + private const val TAG = "CameraXSession"
  298 + private val cameraXStartTimeMsHistogram = Histogram.createCounts("WebRTC.Android.CameraX.StartTimeMs", 1, 10000, 50)
  299 + private val cameraXStopTimeMsHistogram = Histogram.createCounts("WebRTC.Android.CameraX.StopTimeMs", 1, 10000, 50)
  300 + private val cameraXResolutionHistogram = Histogram.createEnumeration("WebRTC.Android.CameraX.Resolution", CameraEnumerationAndroid.COMMON_RESOLUTIONS.size)
  301 + }
  302 +
  303 + internal enum class SessionState {
  304 + RUNNING, STOPPED
  305 + }
  306 +
  307 + internal enum class StabilizationMode {
  308 + OPTICAL, VIDEO, NONE
  309 + }
  310 +
  311 + interface CameraControlListener {
  312 + fun onCameraControlAvailable(control: CameraControl)
  313 + }
  314 +}
@@ -58,6 +58,7 @@ dependencies { @@ -58,6 +58,7 @@ dependencies {
58 // If building the sample app outside the context of this repo, replace the following with: 58 // If building the sample app outside the context of this repo, replace the following with:
59 // api "io.livekit:livekit-android:<version>" 59 // api "io.livekit:livekit-android:<version>"
60 api project(":livekit-android-sdk") 60 api project(":livekit-android-sdk")
  61 + api project(":livekit-android-camerax")
61 62
62 api libs.androidx.core.ktx 63 api libs.androidx.core.ktx
63 api libs.appcompat 64 api libs.appcompat
@@ -67,6 +68,7 @@ dependencies { @@ -67,6 +68,7 @@ dependencies {
67 api libs.androidx.lifecycle.runtime.ktx 68 api libs.androidx.lifecycle.runtime.ktx
68 api libs.androidx.lifecycle.viewmodel.ktx 69 api libs.androidx.lifecycle.viewmodel.ktx
69 api libs.androidx.lifecycle.common.java8 70 api libs.androidx.lifecycle.common.java8
  71 + api libs.androidx.lifecycle.process
70 api libs.protobuf.javalite 72 api libs.protobuf.javalite
71 api libs.androidx.preference.ktx 73 api libs.androidx.preference.ktx
72 // debugImplementation because LeakCanary should only run in debug builds. 74 // debugImplementation because LeakCanary should only run in debug builds.
@@ -39,6 +39,14 @@ dependencies { @@ -39,6 +39,14 @@ dependencies {
39 // as well as classes common to both sample apps. 39 // as well as classes common to both sample apps.
40 implementation project(":sample-app-common") 40 implementation project(":sample-app-common")
41 41
  42 + def camerax_version = "1.2.2"
  43 + implementation "androidx.camera:camera-core:${camerax_version}"
  44 + implementation "androidx.camera:camera-camera2:${camerax_version}"
  45 + implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  46 + implementation "androidx.camera:camera-video:${camerax_version}"
  47 +
  48 + implementation "androidx.camera:camera-view:${camerax_version}"
  49 + implementation "androidx.camera:camera-extensions:${camerax_version}"
42 implementation fileTree(dir: 'libs', include: ['*.jar']) 50 implementation fileTree(dir: 'libs', include: ['*.jar'])
43 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 51 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
44 implementation libs.coroutines.lib 52 implementation libs.coroutines.lib
@@ -26,9 +26,11 @@ import android.widget.Toast @@ -26,9 +26,11 @@ import android.widget.Toast
26 import androidx.activity.result.contract.ActivityResultContracts 26 import androidx.activity.result.contract.ActivityResultContracts
27 import androidx.appcompat.app.AlertDialog 27 import androidx.appcompat.app.AlertDialog
28 import androidx.appcompat.app.AppCompatActivity 28 import androidx.appcompat.app.AppCompatActivity
  29 +import androidx.camera.core.CameraControl
29 import androidx.lifecycle.lifecycleScope 30 import androidx.lifecycle.lifecycleScope
30 import androidx.recyclerview.widget.LinearLayoutManager 31 import androidx.recyclerview.widget.LinearLayoutManager
31 import com.xwray.groupie.GroupieAdapter 32 import com.xwray.groupie.GroupieAdapter
  33 +import io.livekit.android.room.track.video.CameraCapturerUtils
32 import io.livekit.android.sample.common.R 34 import io.livekit.android.sample.common.R
33 import io.livekit.android.sample.databinding.CallActivityBinding 35 import io.livekit.android.sample.databinding.CallActivityBinding
34 import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog 36 import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog
@@ -37,9 +39,14 @@ import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog @@ -37,9 +39,14 @@ import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
37 import io.livekit.android.sample.model.StressTest 39 import io.livekit.android.sample.model.StressTest
38 import kotlinx.coroutines.flow.collectLatest 40 import kotlinx.coroutines.flow.collectLatest
39 import kotlinx.parcelize.Parcelize 41 import kotlinx.parcelize.Parcelize
  42 +import livekit.org.webrtc.CameraXHelper
  43 +import livekit.org.webrtc.CameraXSession
40 44
41 class CallActivity : AppCompatActivity() { 45 class CallActivity : AppCompatActivity() {
42 46
  47 + private var cameraProvider: CameraCapturerUtils.CameraProvider? = null
  48 + private var cameraControl: CameraControl? = null
  49 +
43 private val viewModel: CallViewModel by viewModelByFactory { 50 private val viewModel: CallViewModel by viewModelByFactory {
44 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) 51 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
45 ?: throw NullPointerException("args is null!") 52 ?: throw NullPointerException("args is null!")
@@ -66,6 +73,7 @@ class CallActivity : AppCompatActivity() { @@ -66,6 +73,7 @@ class CallActivity : AppCompatActivity() {
66 viewModel.startScreenCapture(data) 73 viewModel.startScreenCapture(data)
67 } 74 }
68 75
  76 + @androidx.camera.camera2.interop.ExperimentalCamera2Interop
69 override fun onCreate(savedInstanceState: Bundle?) { 77 override fun onCreate(savedInstanceState: Bundle?) {
70 super.onCreate(savedInstanceState) 78 super.onCreate(savedInstanceState)
71 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 79 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -73,6 +81,22 @@ class CallActivity : AppCompatActivity() { @@ -73,6 +81,22 @@ class CallActivity : AppCompatActivity() {
73 81
74 setContentView(binding.root) 82 setContentView(binding.root)
75 83
  84 + val controlListener = object : CameraXSession.CameraControlListener {
  85 + override fun onCameraControlAvailable(control: CameraControl) {
  86 + cameraControl = control
  87 + }
  88 + }
  89 +
  90 + CameraXHelper.getCameraProvider(
  91 + this,
  92 + controlListener,
  93 + ).let {
  94 + if (it.isSupported(this@CallActivity)) {
  95 + CameraCapturerUtils.registerCameraProvider(it)
  96 + cameraProvider = it
  97 + }
  98 + }
  99 +
76 // Audience row setup 100 // Audience row setup
77 val audienceAdapter = GroupieAdapter() 101 val audienceAdapter = GroupieAdapter()
78 binding.audienceRow.apply { 102 binding.audienceRow.apply {
@@ -216,6 +240,9 @@ class CallActivity : AppCompatActivity() { @@ -216,6 +240,9 @@ class CallActivity : AppCompatActivity() {
216 override fun onDestroy() { 240 override fun onDestroy() {
217 binding.audienceRow.adapter = null 241 binding.audienceRow.adapter = null
218 binding.speakerView.adapter = null 242 binding.speakerView.adapter = null
  243 + cameraProvider?.let {
  244 + CameraCapturerUtils.unregisterCameraProvider(it)
  245 + }
219 super.onDestroy() 246 super.onDestroy()
220 } 247 }
221 248
@@ -12,3 +12,4 @@ include ':sample-app-basic' @@ -12,3 +12,4 @@ include ':sample-app-basic'
12 include ':sample-app-record-local' 12 include ':sample-app-record-local'
13 include ':examples:selfie-segmentation' 13 include ':examples:selfie-segmentation'
14 include ':livekit-android-test' 14 include ':livekit-android-test'
  15 +include ':livekit-android-camerax'