@@ -32,33 +32,49 @@ import android.os.Build
3232import android.os.Handler
3333import android.os.HandlerThread
3434import android.util.Log
35+ import android.util.Size
3536import android.view.Surface
3637import androidx.camera.viewfinder.core.TransformationInfo
37- import androidx.camera.viewfinder.core.ViewfinderSurfaceSession
38+ import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest
3839import androidx.camera.viewfinder.core.camera2.Camera2TransformationInfo
39- import androidx.camera.viewfinder.view.ViewfinderView
4040import androidx.compose.runtime.Stable
41+ import androidx.compose.runtime.getValue
42+ import androidx.compose.runtime.mutableStateOf
43+ import androidx.compose.runtime.setValue
4144import androidx.compose.ui.geometry.Offset
45+ import kotlinx.coroutines.NonCancellable
46+ import kotlinx.coroutines.awaitCancellation
47+ import kotlinx.coroutines.suspendCancellableCoroutine
48+ import kotlinx.coroutines.withContext
4249import java.util.concurrent.Executor
50+ import kotlin.coroutines.resume
4351
4452private const val TAG = " BaseCamera2Controller"
4553
4654/* *
47- * Captures the Camera2 plumbing shared by every Camera2 sample: a background [HandlerThread],
48- * camera discovery/open by lens facing, the viewfinder transformation math, tap-to-focus, and
55+ * Captures the Camera2 plumbing shared by every Camera2 sample: a background [HandlerThread], camera
56+ * discovery/open by lens facing, the viewfinder transformation math, tap-to-focus, and
4957 * cross-API-level capture-session creation.
5058 *
51- * Subclasses implement [onCameraOpened] to build their preview (and any extra targets such as an
52- * `ImageReader` or `MediaRecorder` surface), and may override [onCameraPrepared] / [onCameraClosed]
53- * to allocate and release their own resources.
59+ * Preview is Compose-first: the controller exposes a [surfaceRequest] and [transformationInfo] as
60+ * state, and the [Camera2Preview] composable renders them through the Compose `Viewfinder`. When the
61+ * viewfinder hands back a [Surface], [onCameraOpened] is invoked so the subclass can build its
62+ * preview (and any extra targets such as an `ImageReader`). Subclasses may override [onCameraPrepared]
63+ * / [onCameraClosed] to allocate and release their own resources, and [previewSize] to request a
64+ * different viewfinder resolution.
5465 */
5566@Stable
5667abstract class BaseCamera2Controller (
5768 protected val context : Context ,
5869 val isFrontCamera : Boolean ,
5970) {
60- /* * The view the camera renders into. Set by the preview composable before [openCamera]. */
61- var viewfinder: ViewfinderView ? = null
71+ /* * The viewfinder surface request, published once the camera opens; null while closed. */
72+ var surfaceRequest: ViewfinderSurfaceRequest ? by mutableStateOf(null )
73+ private set
74+
75+ /* * How the Compose `Viewfinder` should rotate/mirror the preview for the current device. */
76+ var transformationInfo: TransformationInfo by mutableStateOf(TransformationInfo .DEFAULT )
77+ private set
6278
6379 protected val cameraManager: CameraManager =
6480 context.getSystemService(Context .CAMERA_SERVICE ) as CameraManager
@@ -69,14 +85,17 @@ abstract class BaseCamera2Controller(
6985 protected var cameraDevice: CameraDevice ? = null
7086 protected var captureSession: CameraCaptureSession ? = null
7187 protected var previewRequestBuilder: CaptureRequest .Builder ? = null
72- protected var surfaceSession: ViewfinderSurfaceSession ? = null
7388
7489 protected var cameraId: String = " "
7590 protected var currentCharacteristics: CameraCharacteristics ? = null
7691 protected var currentDisplayRotation: Int = Surface .ROTATION_0
7792
93+ private var previewSurface: Surface ? = null
7894 private var isCameraOpeningOrOpen: Boolean = false
7995
96+ /* * The resolution requested for the preview viewfinder. Subclasses may override. */
97+ protected open val previewSize: Size = Size (1920 , 1080 )
98+
8099 /* * Resolves the camera id matching the requested lens facing, or `null` if none. */
81100 private fun findCameraId (): String? {
82101 val targetFacing =
@@ -92,23 +111,25 @@ abstract class BaseCamera2Controller(
92111 }
93112 }
94113
95- /* * Updates the viewfinder transform so the preview is oriented for the current rotation . */
96- fun updateTransformationInfo (displayRotation : Int ) {
114+ /* * Updates the preview transform for the current device display rotation (a `Surface.ROTATION_*`) . */
115+ fun updateDisplayRotation (displayRotation : Int ) {
97116 currentDisplayRotation = displayRotation
98- val characteristics = currentCharacteristics ? : return
99- val currentViewfinder = viewfinder ? : return
117+ recomputeTransformation()
118+ }
100119
120+ private fun recomputeTransformation () {
121+ val characteristics = currentCharacteristics ? : return
101122 val sensorRotation = characteristics.get(CameraCharacteristics .SENSOR_ORIENTATION ) ? : 0
102123 val lensFacing =
103124 characteristics.get(CameraCharacteristics .LENS_FACING )
104125 ? : CameraCharacteristics .LENS_FACING_BACK
105126 val sign = if (lensFacing == CameraCharacteristics .LENS_FACING_FRONT ) 1 else - 1
106127
107- val rotationDegrees = displayRotation .toRotationDegrees()
128+ val rotationDegrees = currentDisplayRotation .toRotationDegrees()
108129 val relativeRotation = (sensorRotation - rotationDegrees * sign + 360 ) % 360
109130
110131 val baseInfo = Camera2TransformationInfo .createFromCharacteristics(characteristics)
111- currentViewfinder. transformationInfo =
132+ transformationInfo =
112133 TransformationInfo (
113134 sourceRotation = relativeRotation,
114135 isSourceMirroredHorizontally = baseInfo.isSourceMirroredHorizontally,
@@ -129,14 +150,14 @@ abstract class BaseCamera2Controller(
129150 @SuppressLint(" MissingPermission" )
130151 private fun openCameraLocked () {
131152 if (cameraDevice != null || isCameraOpeningOrOpen) return
132- val currentViewfinder = viewfinder ? : return
133153
134154 try {
135155 val id = findCameraId() ? : return
136156 cameraId = id
137157 val characteristics = cameraManager.getCameraCharacteristics(id)
138158 currentCharacteristics = characteristics
139159 onCameraPrepared(characteristics)
160+ recomputeTransformation()
140161 isCameraOpeningOrOpen = true
141162
142163 cameraManager.openCamera(
@@ -145,8 +166,10 @@ abstract class BaseCamera2Controller(
145166 override fun onOpened (camera : CameraDevice ) {
146167 isCameraOpeningOrOpen = false
147168 cameraDevice = camera
148- updateTransformationInfo(currentDisplayRotation)
149- onCameraOpened(camera, currentViewfinder)
169+ // Publishing the request makes the Compose Viewfinder render and hand us a
170+ // surface (via Camera2Preview -> runViewfinderSession).
171+ surfaceRequest = ViewfinderSurfaceRequest (previewSize.width, previewSize.height)
172+ maybeStartSession()
150173 }
151174
152175 override fun onDisconnected (camera : CameraDevice ) {
@@ -171,13 +194,56 @@ abstract class BaseCamera2Controller(
171194 }
172195 }
173196
197+ /* *
198+ * Runs the viewfinder surface session, called by [Camera2Preview] from the Compose `Viewfinder`
199+ * once a [surface] is available. Starts the preview session and keeps the surface alive until the
200+ * viewfinder leaves composition, then tears the session down before the surface is released.
201+ */
202+ suspend fun runViewfinderSession (surface : Surface ) {
203+ backgroundHandler.post {
204+ previewSurface = surface
205+ maybeStartSession()
206+ }
207+ try {
208+ awaitCancellation()
209+ } finally {
210+ withContext(NonCancellable ) {
211+ suspendCancellableCoroutine { continuation ->
212+ backgroundHandler.post {
213+ try {
214+ captureSession?.close()
215+ } catch (e: Exception ) {
216+ Log .w(TAG , " Exception closing capture session" , e)
217+ }
218+ captureSession = null
219+ if (previewSurface == = surface) previewSurface = null
220+ continuation.resume(Unit )
221+ }
222+ }
223+ }
224+ }
225+ }
226+
227+ /* * Starts the preview session once both the device is open and a viewfinder surface exists. */
228+ private fun maybeStartSession () {
229+ val camera = cameraDevice ? : return
230+ val surface = previewSurface ? : return
231+ try {
232+ captureSession?.close()
233+ } catch (e: Exception ) {
234+ Log .w(TAG , " Exception closing previous session" , e)
235+ }
236+ captureSession = null
237+ onCameraOpened(camera, surface)
238+ }
239+
174240 /* * Allocate per-session resources (e.g. an `ImageReader`) before the device is opened. */
175241 protected open fun onCameraPrepared (characteristics : CameraCharacteristics ) {}
176242
177- /* * Build and start the preview session once the [camera] device is open . */
243+ /* * Build and start the preview session for the open [camera] using the viewfinder [surface] . */
178244 protected abstract fun onCameraOpened (
179245 camera : CameraDevice ,
180- viewfinder : ViewfinderView ,
246+ surface : Surface ,
181247 )
182248
183249 /* * Release per-session resources allocated in [onCameraPrepared]. */
@@ -191,12 +257,16 @@ abstract class BaseCamera2Controller(
191257
192258 private fun closeCameraLocked () {
193259 isCameraOpeningOrOpen = false
194- captureSession?.close()
260+ try {
261+ captureSession?.close()
262+ } catch (e: Exception ) {
263+ Log .w(TAG , " Exception closing capture session" , e)
264+ }
195265 captureSession = null
196266 cameraDevice?.close()
197267 cameraDevice = null
198- surfaceSession?.close()
199- surfaceSession = null
268+ previewSurface = null
269+ surfaceRequest = null
200270 onCameraClosed()
201271 }
202272
0 commit comments