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>
正在显示
15 个修改的文件
包含
731 行增加
和
2 行删除
| @@ -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" } |
livekit-android-camerax/.gitignore
0 → 100644
| 1 | +/build |
livekit-android-camerax/build.gradle
0 → 100644
| 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 | +} |
livekit-android-camerax/consumer-rules.pro
0 → 100644
livekit-android-camerax/gradle.properties
0 → 100644
livekit-android-camerax/proguard-rules.pro
0 → 100644
| 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 | +/* | ||
| 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' |
-
请 注册 或 登录 后发表评论