davidliu
Committed by GitHub

CameraX zoom feature and cleanup (#422)

* Clean up and example gesture zoom feature

* spotless

* fix build
  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 io.livekit.android.camerax.ui
  18 +
  19 +import android.content.Context
  20 +import android.view.ScaleGestureDetector
  21 +import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
  22 +import androidx.camera.core.Camera
  23 +import io.livekit.android.room.track.LocalVideoTrack
  24 +import io.livekit.android.util.LKLog
  25 +import kotlinx.coroutines.flow.StateFlow
  26 +import livekit.org.webrtc.getCameraX
  27 +
  28 +class ScaleZoomHelper(
  29 + private val cameraFlow: StateFlow<Camera?>?,
  30 +) {
  31 + constructor(localVideoTrack: LocalVideoTrack) : this(localVideoTrack.capturer.getCameraX())
  32 +
  33 + init {
  34 + if (cameraFlow != null) {
  35 + LKLog.w { "null camera flow passed in to ScaleZoomHelper, zoom is disabled." }
  36 + }
  37 + }
  38 +
  39 + fun zoom(factor: Float) {
  40 + val camera = cameraFlow?.value ?: return
  41 + val zoomState = camera.cameraInfo.zoomState.value ?: return
  42 + val currentZoom = zoomState.zoomRatio
  43 + val newZoom = (currentZoom * factor).coerceIn(zoomState.minZoomRatio, zoomState.maxZoomRatio)
  44 +
  45 + if (newZoom != currentZoom) {
  46 + camera.cameraControl.setZoomRatio(newZoom)
  47 + }
  48 + }
  49 +
  50 + companion object {
  51 + fun createGestureDetector(context: Context, localVideoTrack: LocalVideoTrack): ScaleGestureDetector {
  52 + return createGestureDetector(context, localVideoTrack.capturer.getCameraX())
  53 + }
  54 +
  55 + fun createGestureDetector(context: Context, cameraFlow: StateFlow<Camera?>?): ScaleGestureDetector {
  56 + val helper = ScaleZoomHelper(cameraFlow)
  57 +
  58 + return ScaleGestureDetector(
  59 + context,
  60 + object : SimpleOnScaleGestureListener() {
  61 + override fun onScale(detector: ScaleGestureDetector): Boolean {
  62 + helper.zoom(detector.scaleFactor)
  63 + return true
  64 + }
  65 + },
  66 + ).apply {
  67 + isQuickScaleEnabled = false
  68 + }
  69 + }
  70 + }
  71 +}
@@ -18,10 +18,16 @@ package livekit.org.webrtc @@ -18,10 +18,16 @@ package livekit.org.webrtc
18 18
19 import android.content.Context 19 import android.content.Context
20 import android.hardware.camera2.CameraManager 20 import android.hardware.camera2.CameraManager
  21 +import androidx.annotation.OptIn
21 import androidx.camera.camera2.interop.ExperimentalCamera2Interop 22 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
  23 +import androidx.camera.core.Camera
22 import androidx.lifecycle.LifecycleOwner 24 import androidx.lifecycle.LifecycleOwner
23 import io.livekit.android.room.track.video.CameraCapturerWithSize 25 import io.livekit.android.room.track.video.CameraCapturerWithSize
24 import io.livekit.android.room.track.video.CameraEventsDispatchHandler 26 import io.livekit.android.room.track.video.CameraEventsDispatchHandler
  27 +import io.livekit.android.util.FlowObservable
  28 +import io.livekit.android.util.flow
  29 +import io.livekit.android.util.flowDelegate
  30 +import kotlinx.coroutines.flow.StateFlow
25 31
26 @ExperimentalCamera2Interop 32 @ExperimentalCamera2Interop
27 internal class CameraXCapturer( 33 internal class CameraXCapturer(
@@ -29,9 +35,11 @@ internal class CameraXCapturer( @@ -29,9 +35,11 @@ internal class CameraXCapturer(
29 private val lifecycleOwner: LifecycleOwner, 35 private val lifecycleOwner: LifecycleOwner,
30 cameraName: String?, 36 cameraName: String?,
31 eventsHandler: CameraVideoCapturer.CameraEventsHandler?, 37 eventsHandler: CameraVideoCapturer.CameraEventsHandler?,
32 -) : CameraCapturer(cameraName, eventsHandler, Camera2Enumerator(context)) { 38 +) : CameraCapturer(cameraName, eventsHandler, CameraXEnumerator(context, lifecycleOwner)) {
33 39
34 - var cameraControlListener: CameraXSession.CameraControlListener? = null 40 + @FlowObservable
  41 + @get:FlowObservable
  42 + var currentCamera by flowDelegate<Camera?>(null)
35 43
36 override fun createCameraSession( 44 override fun createCameraSession(
37 createSessionCallback: CameraSession.CreateSessionCallback, 45 createSessionCallback: CameraSession.CreateSessionCallback,
@@ -44,8 +52,37 @@ internal class CameraXCapturer( @@ -44,8 +52,37 @@ internal class CameraXCapturer(
44 framerate: Int, 52 framerate: Int,
45 ) { 53 ) {
46 CameraXSession( 54 CameraXSession(
47 - createSessionCallback,  
48 - events, 55 + object : CameraSession.CreateSessionCallback {
  56 + override fun onDone(session: CameraSession) {
  57 + createSessionCallback.onDone(session)
  58 + currentCamera = (session as CameraXSession).camera
  59 + }
  60 +
  61 + override fun onFailure(failureType: CameraSession.FailureType, error: String) {
  62 + createSessionCallback.onFailure(failureType, error)
  63 + }
  64 + },
  65 + object : CameraSession.Events {
  66 + override fun onCameraOpening() {
  67 + events.onCameraOpening()
  68 + }
  69 +
  70 + override fun onCameraError(session: CameraSession, error: String) {
  71 + events.onCameraError(session, error)
  72 + }
  73 +
  74 + override fun onCameraDisconnected(session: CameraSession) {
  75 + events.onCameraDisconnected(session)
  76 + }
  77 +
  78 + override fun onCameraClosed(session: CameraSession) {
  79 + events.onCameraClosed(session)
  80 + }
  81 +
  82 + override fun onFrameCaptured(session: CameraSession, frame: VideoFrame) {
  83 + events.onFrameCaptured(session, frame)
  84 + }
  85 + },
49 applicationContext, 86 applicationContext,
50 lifecycleOwner, 87 lifecycleOwner,
51 surfaceTextureHelper, 88 surfaceTextureHelper,
@@ -53,14 +90,13 @@ internal class CameraXCapturer( @@ -53,14 +90,13 @@ internal class CameraXCapturer(
53 width, 90 width,
54 height, 91 height,
55 framerate, 92 framerate,
56 - cameraControlListener,  
57 ) 93 )
58 } 94 }
59 } 95 }
60 96
61 @ExperimentalCamera2Interop 97 @ExperimentalCamera2Interop
62 internal class CameraXCapturerWithSize( 98 internal class CameraXCapturerWithSize(
63 - private val capturer: CameraXCapturer, 99 + internal val capturer: CameraXCapturer,
64 private val cameraManager: CameraManager, 100 private val cameraManager: CameraManager,
65 private val deviceName: String?, 101 private val deviceName: String?,
66 cameraEventsDispatchHandler: CameraEventsDispatchHandler, 102 cameraEventsDispatchHandler: CameraEventsDispatchHandler,
@@ -69,3 +105,20 @@ internal class CameraXCapturerWithSize( @@ -69,3 +105,20 @@ internal class CameraXCapturerWithSize(
69 return CameraXHelper.findClosestCaptureFormat(cameraManager, deviceName, width, height) 105 return CameraXHelper.findClosestCaptureFormat(cameraManager, deviceName, width, height)
70 } 106 }
71 } 107 }
  108 +
  109 +/**
  110 + * Gets the [androidx.camera.core.Camera] from the VideoCapturer if it's using CameraX.
  111 + */
  112 +@OptIn(ExperimentalCamera2Interop::class)
  113 +fun VideoCapturer.getCameraX(): StateFlow<Camera?>? {
  114 + val actualCapturer = if (this is CameraXCapturerWithSize) {
  115 + this.capturer
  116 + } else {
  117 + this
  118 + }
  119 +
  120 + if (actualCapturer is CameraXCapturer) {
  121 + return actualCapturer::currentCamera.flow
  122 + }
  123 + return null
  124 +}
@@ -26,6 +26,9 @@ import androidx.camera.camera2.interop.Camera2CameraInfo @@ -26,6 +26,9 @@ import androidx.camera.camera2.interop.Camera2CameraInfo
26 import androidx.camera.camera2.interop.ExperimentalCamera2Interop 26 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
27 import androidx.lifecycle.LifecycleOwner 27 import androidx.lifecycle.LifecycleOwner
28 28
  29 +/**
  30 + * @suppress
  31 + */
29 @ExperimentalCamera2Interop 32 @ExperimentalCamera2Interop
30 class CameraXEnumerator( 33 class CameraXEnumerator(
31 context: Context, 34 context: Context,
@@ -28,10 +28,14 @@ import io.livekit.android.room.track.video.CameraEventsDispatchHandler @@ -28,10 +28,14 @@ import io.livekit.android.room.track.video.CameraEventsDispatchHandler
28 class CameraXHelper { 28 class CameraXHelper {
29 companion object { 29 companion object {
30 30
  31 + /**
  32 + * Gets a CameraProvider that uses CameraX for its sessions.
  33 + *
  34 + * For use with [CameraCapturerUtils.registerCameraProvider].
  35 + */
31 @ExperimentalCamera2Interop 36 @ExperimentalCamera2Interop
32 fun getCameraProvider( 37 fun getCameraProvider(
33 lifecycleOwner: LifecycleOwner, 38 lifecycleOwner: LifecycleOwner,
34 - controlListener: CameraXSession.CameraControlListener?,  
35 ) = object : CameraCapturerUtils.CameraProvider { 39 ) = object : CameraCapturerUtils.CameraProvider {
36 40
37 private var enumerator: CameraXEnumerator? = null 41 private var enumerator: CameraXEnumerator? = null
@@ -51,9 +55,7 @@ class CameraXHelper { @@ -51,9 +55,7 @@ class CameraXHelper {
51 val enumerator = provideEnumerator(context) 55 val enumerator = provideEnumerator(context)
52 val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) 56 val targetDeviceName = enumerator.findCamera(options.deviceId, options.position)
53 val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) as CameraXCapturer 57 val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, eventsHandler) as CameraXCapturer
54 - controlListener?.let {  
55 - targetVideoCapturer.cameraControlListener = it  
56 - } 58 +
57 return CameraXCapturerWithSize( 59 return CameraXCapturerWithSize(
58 targetVideoCapturer, 60 targetVideoCapturer,
59 context.getSystemService(Context.CAMERA_SERVICE) as CameraManager, 61 context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,
@@ -42,6 +42,9 @@ import livekit.org.webrtc.CameraEnumerationAndroid.CaptureFormat @@ -42,6 +42,9 @@ import livekit.org.webrtc.CameraEnumerationAndroid.CaptureFormat
42 import java.util.concurrent.Executor 42 import java.util.concurrent.Executor
43 import java.util.concurrent.TimeUnit 43 import java.util.concurrent.TimeUnit
44 44
  45 +/**
  46 + * @suppress
  47 + */
45 @androidx.camera.camera2.interop.ExperimentalCamera2Interop 48 @androidx.camera.camera2.interop.ExperimentalCamera2Interop
46 class CameraXSession 49 class CameraXSession
47 internal constructor( 50 internal constructor(
@@ -54,14 +57,14 @@ internal constructor( @@ -54,14 +57,14 @@ internal constructor(
54 private val width: Int, 57 private val width: Int,
55 private val height: Int, 58 private val height: Int,
56 private val frameRate: Int, 59 private val frameRate: Int,
57 - private val cameraControlListener: CameraControlListener? = null,  
58 ) : CameraSession { 60 ) : CameraSession {
59 61
60 private var state = SessionState.RUNNING 62 private var state = SessionState.RUNNING
61 private var cameraThreadHandler = surfaceTextureHelper.handler 63 private var cameraThreadHandler = surfaceTextureHelper.handler
62 private lateinit var cameraProvider: ProcessCameraProvider 64 private lateinit var cameraProvider: ProcessCameraProvider
63 private lateinit var surfaceProvider: SurfaceProvider 65 private lateinit var surfaceProvider: SurfaceProvider
64 - private var camera: Camera? = null 66 + var camera: Camera? = null
  67 + private set
65 private var surface: Surface? = null 68 private var surface: Surface? = null
66 private var cameraOrientation: Int = 0 69 private var cameraOrientation: Int = 0
67 private var isCameraFrontFacing: Boolean = true 70 private var isCameraFrontFacing: Boolean = true
@@ -168,11 +171,12 @@ internal constructor( @@ -168,11 +171,12 @@ internal constructor(
168 cameraProvider.unbindAll() 171 cameraProvider.unbindAll()
169 172
170 // Bind use cases to camera 173 // Bind use cases to camera
171 - camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview).apply {  
172 - cameraControlListener?.onCameraControlAvailable(this.cameraControl) 174 + camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview)
  175 +
  176 + cameraThreadHandler.post {
  177 + sessionCallback.onDone(this@CameraXSession)
173 } 178 }
174 } 179 }
175 - sessionCallback.onDone(this@CameraXSession)  
176 } catch (e: Exception) { 180 } catch (e: Exception) {
177 reportError("Failed to open camera: $e") 181 reportError("Failed to open camera: $e")
178 } 182 }
@@ -64,7 +64,7 @@ import livekit.LivekitModels.VideoQuality as ProtoVideoQuality @@ -64,7 +64,7 @@ import livekit.LivekitModels.VideoQuality as ProtoVideoQuality
64 open class LocalVideoTrack 64 open class LocalVideoTrack
65 @AssistedInject 65 @AssistedInject
66 constructor( 66 constructor(
67 - @Assisted private var capturer: VideoCapturer, 67 + @Assisted capturer: VideoCapturer,
68 @Assisted private var source: VideoSource, 68 @Assisted private var source: VideoSource,
69 @Assisted name: String, 69 @Assisted name: String,
70 @Assisted options: LocalVideoTrackOptions, 70 @Assisted options: LocalVideoTrackOptions,
@@ -81,6 +81,9 @@ constructor( @@ -81,6 +81,9 @@ constructor(
81 @Assisted private var dispatchObserver: CaptureDispatchObserver? = null, 81 @Assisted private var dispatchObserver: CaptureDispatchObserver? = null,
82 ) : VideoTrack(name, rtcTrack) { 82 ) : VideoTrack(name, rtcTrack) {
83 83
  84 + var capturer = capturer
  85 + private set
  86 +
84 override var rtcTrack: livekit.org.webrtc.VideoTrack = rtcTrack 87 override var rtcTrack: livekit.org.webrtc.VideoTrack = rtcTrack
85 internal set 88 internal set
86 89
@@ -73,8 +73,11 @@ val <T> KProperty0<T>.flow: StateFlow<T> @@ -73,8 +73,11 @@ val <T> KProperty0<T>.flow: StateFlow<T>
73 @MustBeDocumented 73 @MustBeDocumented
74 annotation class FlowObservable 74 annotation class FlowObservable
75 75
  76 +/**
  77 + * @suppress
  78 + */
76 @FlowObservable 79 @FlowObservable
77 -internal class MutableStateFlowDelegate<T> 80 +class MutableStateFlowDelegate<T>
78 internal constructor( 81 internal constructor(
79 private val flow: MutableStateFlow<T>, 82 private val flow: MutableStateFlow<T>,
80 private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null, 83 private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null,
@@ -94,8 +97,11 @@ internal constructor( @@ -94,8 +97,11 @@ internal constructor(
94 } 97 }
95 } 98 }
96 99
  100 +/**
  101 + * @suppress
  102 + */
97 @FlowObservable 103 @FlowObservable
98 -internal class StateFlowDelegate<T> 104 +class StateFlowDelegate<T>
99 internal constructor( 105 internal constructor(
100 private val flow: StateFlow<T>, 106 private val flow: StateFlow<T>,
101 ) : StateFlow<T> by flow { 107 ) : StateFlow<T> by flow {
@@ -108,14 +114,20 @@ internal constructor( @@ -108,14 +114,20 @@ internal constructor(
108 } 114 }
109 } 115 }
110 116
111 -internal fun <T> flowDelegate( 117 +/**
  118 + * @suppress
  119 + */
  120 +fun <T> flowDelegate(
112 initialValue: T, 121 initialValue: T,
113 onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null, 122 onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null,
114 ): MutableStateFlowDelegate<T> { 123 ): MutableStateFlowDelegate<T> {
115 return MutableStateFlowDelegate(MutableStateFlow(initialValue), onSetValue) 124 return MutableStateFlowDelegate(MutableStateFlow(initialValue), onSetValue)
116 } 125 }
117 126
118 -internal fun <T> flowDelegate( 127 +/**
  128 + * @suppress
  129 + */
  130 +fun <T> flowDelegate(
119 stateFlow: StateFlow<T>, 131 stateFlow: StateFlow<T>,
120 ): StateFlowDelegate<T> { 132 ): StateFlowDelegate<T> {
121 return StateFlowDelegate(stateFlow) 133 return StateFlowDelegate(stateFlow)
@@ -21,9 +21,12 @@ import android.app.Application @@ -21,9 +21,12 @@ import android.app.Application
21 import android.content.Intent 21 import android.content.Intent
22 import android.media.projection.MediaProjectionManager 22 import android.media.projection.MediaProjectionManager
23 import android.os.Build 23 import android.os.Build
  24 +import androidx.annotation.OptIn
  25 +import androidx.camera.camera2.interop.ExperimentalCamera2Interop
24 import androidx.lifecycle.AndroidViewModel 26 import androidx.lifecycle.AndroidViewModel
25 import androidx.lifecycle.LiveData 27 import androidx.lifecycle.LiveData
26 import androidx.lifecycle.MutableLiveData 28 import androidx.lifecycle.MutableLiveData
  29 +import androidx.lifecycle.ProcessLifecycleOwner
27 import androidx.lifecycle.viewModelScope 30 import androidx.lifecycle.viewModelScope
28 import com.github.ajalt.timberkt.Timber 31 import com.github.ajalt.timberkt.Timber
29 import io.livekit.android.AudioOptions 32 import io.livekit.android.AudioOptions
@@ -43,6 +46,7 @@ import io.livekit.android.room.track.CameraPosition @@ -43,6 +46,7 @@ import io.livekit.android.room.track.CameraPosition
43 import io.livekit.android.room.track.LocalScreencastVideoTrack 46 import io.livekit.android.room.track.LocalScreencastVideoTrack
44 import io.livekit.android.room.track.LocalVideoTrack 47 import io.livekit.android.room.track.LocalVideoTrack
45 import io.livekit.android.room.track.Track 48 import io.livekit.android.room.track.Track
  49 +import io.livekit.android.room.track.video.CameraCapturerUtils
46 import io.livekit.android.sample.model.StressTest 50 import io.livekit.android.sample.model.StressTest
47 import io.livekit.android.sample.service.ForegroundService 51 import io.livekit.android.sample.service.ForegroundService
48 import io.livekit.android.util.LKLog 52 import io.livekit.android.util.LKLog
@@ -57,7 +61,9 @@ import kotlinx.coroutines.flow.combine @@ -57,7 +61,9 @@ import kotlinx.coroutines.flow.combine
57 import kotlinx.coroutines.flow.map 61 import kotlinx.coroutines.flow.map
58 import kotlinx.coroutines.isActive 62 import kotlinx.coroutines.isActive
59 import kotlinx.coroutines.launch 63 import kotlinx.coroutines.launch
  64 +import livekit.org.webrtc.CameraXHelper
60 65
  66 +@OptIn(ExperimentalCamera2Interop::class)
61 class CallViewModel( 67 class CallViewModel(
62 val url: String, 68 val url: String,
63 val token: String, 69 val token: String,
@@ -93,6 +99,7 @@ class CallViewModel( @@ -93,6 +99,7 @@ class CallViewModel(
93 ), 99 ),
94 ) 100 )
95 101
  102 + private var cameraProvider: CameraCapturerUtils.CameraProvider? = null
96 val audioHandler = room.audioHandler as AudioSwitchHandler 103 val audioHandler = room.audioHandler as AudioSwitchHandler
97 104
98 val participants = room::remoteParticipants.flow 105 val participants = room::remoteParticipants.flow
@@ -139,6 +146,14 @@ class CallViewModel( @@ -139,6 +146,14 @@ class CallViewModel(
139 val permissionAllowed = mutablePermissionAllowed.hide() 146 val permissionAllowed = mutablePermissionAllowed.hide()
140 147
141 init { 148 init {
  149 +
  150 + CameraXHelper.getCameraProvider(ProcessLifecycleOwner.get()).let {
  151 + if (it.isSupported(application)) {
  152 + CameraCapturerUtils.registerCameraProvider(it)
  153 + cameraProvider = it
  154 + }
  155 + }
  156 +
142 viewModelScope.launch { 157 viewModelScope.launch {
143 // Collect any errors. 158 // Collect any errors.
144 launch { 159 launch {
@@ -329,6 +344,9 @@ class CallViewModel( @@ -329,6 +344,9 @@ class CallViewModel(
329 val application = getApplication<Application>() 344 val application = getApplication<Application>()
330 val foregroundServiceIntent = Intent(application, ForegroundService::class.java) 345 val foregroundServiceIntent = Intent(application, ForegroundService::class.java)
331 application.stopService(foregroundServiceIntent) 346 application.stopService(foregroundServiceIntent)
  347 + cameraProvider?.let {
  348 + CameraCapturerUtils.unregisterCameraProvider(it)
  349 + }
332 } 350 }
333 351
334 fun setMicEnabled(enabled: Boolean) { 352 fun setMicEnabled(enabled: Boolean) {
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 package io.livekit.android.composesample.ui 17 package io.livekit.android.composesample.ui
18 18
19 import androidx.compose.foundation.background 19 import androidx.compose.foundation.background
  20 +import androidx.compose.foundation.gestures.detectTransformGestures
20 import androidx.compose.foundation.layout.Box 21 import androidx.compose.foundation.layout.Box
21 import androidx.compose.runtime.Composable 22 import androidx.compose.runtime.Composable
22 import androidx.compose.runtime.DisposableEffect 23 import androidx.compose.runtime.DisposableEffect
@@ -27,11 +28,14 @@ import androidx.compose.runtime.remember @@ -27,11 +28,14 @@ import androidx.compose.runtime.remember
27 import androidx.compose.runtime.setValue 28 import androidx.compose.runtime.setValue
28 import androidx.compose.ui.Modifier 29 import androidx.compose.ui.Modifier
29 import androidx.compose.ui.graphics.Color 30 import androidx.compose.ui.graphics.Color
  31 +import androidx.compose.ui.input.pointer.pointerInput
30 import androidx.compose.ui.layout.onGloballyPositioned 32 import androidx.compose.ui.layout.onGloballyPositioned
31 import androidx.compose.ui.platform.LocalView 33 import androidx.compose.ui.platform.LocalView
32 import androidx.compose.ui.viewinterop.AndroidView 34 import androidx.compose.ui.viewinterop.AndroidView
  35 +import io.livekit.android.camerax.ui.ScaleZoomHelper
33 import io.livekit.android.renderer.TextureViewRenderer 36 import io.livekit.android.renderer.TextureViewRenderer
34 import io.livekit.android.room.Room 37 import io.livekit.android.room.Room
  38 +import io.livekit.android.room.track.LocalVideoTrack
35 import io.livekit.android.room.track.RemoteVideoTrack 39 import io.livekit.android.room.track.RemoteVideoTrack
36 import io.livekit.android.room.track.VideoTrack 40 import io.livekit.android.room.track.VideoTrack
37 import livekit.org.webrtc.RendererCommon 41 import livekit.org.webrtc.RendererCommon
@@ -63,6 +67,13 @@ fun VideoRenderer( @@ -63,6 +67,13 @@ fun VideoRenderer(
63 return 67 return
64 } 68 }
65 69
  70 + val scaleZoomHelper = remember(room, videoTrack) {
  71 + if (videoTrack is LocalVideoTrack) {
  72 + ScaleZoomHelper(videoTrack)
  73 + } else {
  74 + null
  75 + }
  76 + }
66 val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() } 77 val videoSinkVisibility = remember(room, videoTrack) { ComposeVisibility() }
67 var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) } 78 var boundVideoTrack by remember { mutableStateOf<VideoTrack?>(null) }
68 var view: TextureViewRenderer? by remember { mutableStateOf(null) } 79 var view: TextureViewRenderer? by remember { mutableStateOf(null) }
@@ -127,6 +138,13 @@ fun VideoRenderer( @@ -127,6 +138,13 @@ fun VideoRenderer(
127 }, 138 },
128 update = { v -> setupVideoIfNeeded(videoTrack, v) }, 139 update = { v -> setupVideoIfNeeded(videoTrack, v) },
129 modifier = modifier 140 modifier = modifier
130 - .onGloballyPositioned { videoSinkVisibility.onGloballyPositioned(it) }, 141 + .onGloballyPositioned { videoSinkVisibility.onGloballyPositioned(it) }
  142 + .pointerInput(Unit) {
  143 + detectTransformGestures(
  144 + onGesture = { _, _, zoom, _ ->
  145 + scaleZoomHelper?.zoom(zoom)
  146 + },
  147 + )
  148 + },
131 ) 149 )
132 } 150 }
@@ -26,11 +26,9 @@ import android.widget.Toast @@ -26,11 +26,9 @@ 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  
30 import androidx.lifecycle.lifecycleScope 29 import androidx.lifecycle.lifecycleScope
31 import androidx.recyclerview.widget.LinearLayoutManager 30 import androidx.recyclerview.widget.LinearLayoutManager
32 import com.xwray.groupie.GroupieAdapter 31 import com.xwray.groupie.GroupieAdapter
33 -import io.livekit.android.room.track.video.CameraCapturerUtils  
34 import io.livekit.android.sample.common.R 32 import io.livekit.android.sample.common.R
35 import io.livekit.android.sample.databinding.CallActivityBinding 33 import io.livekit.android.sample.databinding.CallActivityBinding
36 import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog 34 import io.livekit.android.sample.dialog.showAudioProcessorSwitchDialog
@@ -39,14 +37,9 @@ import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog @@ -39,14 +37,9 @@ import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
39 import io.livekit.android.sample.model.StressTest 37 import io.livekit.android.sample.model.StressTest
40 import kotlinx.coroutines.flow.collectLatest 38 import kotlinx.coroutines.flow.collectLatest
41 import kotlinx.parcelize.Parcelize 39 import kotlinx.parcelize.Parcelize
42 -import livekit.org.webrtc.CameraXHelper  
43 -import livekit.org.webrtc.CameraXSession  
44 40
45 class CallActivity : AppCompatActivity() { 41 class CallActivity : AppCompatActivity() {
46 42
47 - private var cameraProvider: CameraCapturerUtils.CameraProvider? = null  
48 - private var cameraControl: CameraControl? = null  
49 -  
50 private val viewModel: CallViewModel by viewModelByFactory { 43 private val viewModel: CallViewModel by viewModelByFactory {
51 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) 44 val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS)
52 ?: throw NullPointerException("args is null!") 45 ?: throw NullPointerException("args is null!")
@@ -81,22 +74,6 @@ class CallActivity : AppCompatActivity() { @@ -81,22 +74,6 @@ class CallActivity : AppCompatActivity() {
81 74
82 setContentView(binding.root) 75 setContentView(binding.root)
83 76
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 -  
100 // Audience row setup 77 // Audience row setup
101 val audienceAdapter = GroupieAdapter() 78 val audienceAdapter = GroupieAdapter()
102 binding.audienceRow.apply { 79 binding.audienceRow.apply {
@@ -240,9 +217,6 @@ class CallActivity : AppCompatActivity() { @@ -240,9 +217,6 @@ class CallActivity : AppCompatActivity() {
240 override fun onDestroy() { 217 override fun onDestroy() {
241 binding.audienceRow.adapter = null 218 binding.audienceRow.adapter = null
242 binding.speakerView.adapter = null 219 binding.speakerView.adapter = null
243 - cameraProvider?.let {  
244 - CameraCapturerUtils.unregisterCameraProvider(it)  
245 - }  
246 super.onDestroy() 220 super.onDestroy()
247 } 221 }
248 222