1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
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 package com.google.jetpackcamera.core.camera
17 
18 import android.Manifest
19 import android.content.ContentValues
20 import android.content.Context
21 import android.content.pm.PackageManager
22 import android.hardware.camera2.CameraCaptureSession
23 import android.hardware.camera2.CaptureRequest
24 import android.hardware.camera2.CaptureResult
25 import android.hardware.camera2.TotalCaptureResult
26 import android.net.Uri
27 import android.os.Build
28 import android.os.SystemClock
29 import android.provider.MediaStore
30 import android.util.Log
31 import android.util.Range
32 import androidx.annotation.OptIn
33 import androidx.camera.camera2.interop.Camera2CameraInfo
34 import androidx.camera.camera2.interop.Camera2Interop
35 import androidx.camera.camera2.interop.ExperimentalCamera2Interop
36 import androidx.camera.core.Camera
37 import androidx.camera.core.CameraControl
38 import androidx.camera.core.CameraEffect
39 import androidx.camera.core.CameraInfo
40 import androidx.camera.core.CameraSelector
41 import androidx.camera.core.FocusMeteringAction
42 import androidx.camera.core.ImageCapture
43 import androidx.camera.core.Preview
44 import androidx.camera.core.SurfaceOrientedMeteringPointFactory
45 import androidx.camera.core.TorchState
46 import androidx.camera.core.UseCaseGroup
47 import androidx.camera.core.ViewPort
48 import androidx.camera.core.resolutionselector.AspectRatioStrategy
49 import androidx.camera.core.resolutionselector.ResolutionSelector
50 import androidx.camera.video.FileOutputOptions
51 import androidx.camera.video.MediaStoreOutputOptions
52 import androidx.camera.video.Recorder
53 import androidx.camera.video.Recording
54 import androidx.camera.video.VideoCapture
55 import androidx.camera.video.VideoRecordEvent
56 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
57 import androidx.concurrent.futures.await
58 import androidx.core.content.ContextCompat
59 import androidx.core.content.ContextCompat.checkSelfPermission
60 import androidx.lifecycle.asFlow
61 import com.google.jetpackcamera.core.camera.effects.SingleSurfaceForcingEffect
62 import com.google.jetpackcamera.settings.model.AspectRatio
63 import com.google.jetpackcamera.settings.model.CaptureMode
64 import com.google.jetpackcamera.settings.model.DeviceRotation
65 import com.google.jetpackcamera.settings.model.DynamicRange
66 import com.google.jetpackcamera.settings.model.FlashMode
67 import com.google.jetpackcamera.settings.model.ImageOutputFormat
68 import com.google.jetpackcamera.settings.model.LensFacing
69 import com.google.jetpackcamera.settings.model.Stabilization
70 import java.io.File
71 import java.util.Date
72 import java.util.concurrent.Executor
73 import kotlin.coroutines.ContinuationInterceptor
74 import kotlin.math.abs
75 import kotlinx.atomicfu.atomic
76 import kotlinx.coroutines.CoroutineDispatcher
77 import kotlinx.coroutines.CoroutineStart
78 import kotlinx.coroutines.Job
79 import kotlinx.coroutines.asExecutor
80 import kotlinx.coroutines.coroutineScope
81 import kotlinx.coroutines.currentCoroutineContext
82 import kotlinx.coroutines.flow.StateFlow
83 import kotlinx.coroutines.flow.collectLatest
84 import kotlinx.coroutines.flow.filterNotNull
85 import kotlinx.coroutines.flow.first
86 import kotlinx.coroutines.flow.map
87 import kotlinx.coroutines.flow.onCompletion
88 import kotlinx.coroutines.flow.update
89 import kotlinx.coroutines.launch
90 
91 private const val TAG = "CameraSession"
92 
93 context(CameraSessionContext)
94 internal suspend fun runSingleCameraSession(
95     sessionSettings: PerpetualSessionSettings.SingleCamera,
96     useCaseMode: CameraUseCase.UseCaseMode,
97     // TODO(tm): ImageCapture should go through an event channel like VideoCapture
98     onImageCaptureCreated: (ImageCapture) -> Unit = {}
<lambda>null99 ) = coroutineScope {
100     val lensFacing = sessionSettings.cameraInfo.appLensFacing
101     Log.d(TAG, "Starting new single camera session for $lensFacing")
102 
103     val initialTransientSettings = transientSettings
104         .filterNotNull()
105         .first()
106 
107     val useCaseGroup = createUseCaseGroup(
108         cameraInfo = sessionSettings.cameraInfo,
109         initialTransientSettings = initialTransientSettings,
110         stabilizePreviewMode = sessionSettings.stabilizePreviewMode,
111         stabilizeVideoMode = sessionSettings.stabilizeVideoMode,
112         aspectRatio = sessionSettings.aspectRatio,
113         targetFrameRate = sessionSettings.targetFrameRate,
114         dynamicRange = sessionSettings.dynamicRange,
115         imageFormat = sessionSettings.imageFormat,
116         useCaseMode = useCaseMode,
117         effect = when (sessionSettings.captureMode) {
118             CaptureMode.SINGLE_STREAM -> SingleSurfaceForcingEffect(this@coroutineScope)
119             CaptureMode.MULTI_STREAM -> null
120         }
121     ).apply {
122         getImageCapture()?.let(onImageCaptureCreated)
123     }
124 
125     cameraProvider.runWith(sessionSettings.cameraInfo.cameraSelector, useCaseGroup) { camera ->
126         Log.d(TAG, "Camera session started")
127 
128         launch {
129             processFocusMeteringEvents(camera.cameraControl)
130         }
131 
132         launch {
133             processVideoControlEvents(
134                 camera,
135                 useCaseGroup.getVideoCapture(),
136                 captureTypeSuffix = when (sessionSettings.captureMode) {
137                     CaptureMode.MULTI_STREAM -> "MultiStream"
138                     CaptureMode.SINGLE_STREAM -> "SingleStream"
139                 }
140             )
141         }
142 
143         launch {
144             camera.cameraInfo.torchState.asFlow().collectLatest { torchState ->
145                 currentCameraState.update { old ->
146                     old.copy(torchEnabled = torchState == TorchState.ON)
147                 }
148             }
149         }
150 
151         applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup)
152         processTransientSettingEvents(
153             camera,
154             useCaseGroup,
155             initialTransientSettings,
156             transientSettings
157         )
158     }
159 }
160 
161 context(CameraSessionContext)
processTransientSettingEventsnull162 internal suspend fun processTransientSettingEvents(
163     camera: Camera,
164     useCaseGroup: UseCaseGroup,
165     initialTransientSettings: TransientSessionSettings,
166     transientSettings: StateFlow<TransientSessionSettings?>
167 ) {
168     var prevTransientSettings = initialTransientSettings
169     transientSettings.filterNotNull().collectLatest { newTransientSettings ->
170         // Apply camera control settings
171         if (prevTransientSettings.zoomScale != newTransientSettings.zoomScale) {
172             camera.cameraInfo.zoomState.value?.let { zoomState ->
173                 val finalScale =
174                     (zoomState.zoomRatio * newTransientSettings.zoomScale).coerceIn(
175                         zoomState.minZoomRatio,
176                         zoomState.maxZoomRatio
177                     )
178                 camera.cameraControl.setZoomRatio(finalScale)
179                 currentCameraState.update { old ->
180                     old.copy(zoomScale = finalScale)
181                 }
182             }
183         }
184 
185         useCaseGroup.getImageCapture()?.let { imageCapture ->
186             if (prevTransientSettings.flashMode != newTransientSettings.flashMode) {
187                 setFlashModeInternal(
188                     imageCapture = imageCapture,
189                     flashMode = newTransientSettings.flashMode,
190                     isFrontFacing = camera.cameraInfo.appLensFacing == LensFacing.FRONT
191                 )
192             }
193         }
194 
195         if (prevTransientSettings.deviceRotation
196             != newTransientSettings.deviceRotation
197         ) {
198             Log.d(
199                 TAG,
200                 "Updating device rotation from " +
201                     "${prevTransientSettings.deviceRotation} -> " +
202                     "${newTransientSettings.deviceRotation}"
203             )
204             applyDeviceRotation(newTransientSettings.deviceRotation, useCaseGroup)
205         }
206 
207         prevTransientSettings = newTransientSettings
208     }
209 }
210 
applyDeviceRotationnull211 internal fun applyDeviceRotation(deviceRotation: DeviceRotation, useCaseGroup: UseCaseGroup) {
212     val targetRotation = deviceRotation.toUiSurfaceRotation()
213     useCaseGroup.useCases.forEach {
214         when (it) {
215             is Preview -> {
216                 // Preview's target rotation should not be updated with device rotation.
217                 // Instead, preview rotation should match the display rotation.
218                 // When Preview is created, it is initialized with the display rotation.
219                 // This will need to be updated separately if the display rotation is not
220                 // locked. Currently the app is locked to portrait orientation.
221             }
222 
223             is ImageCapture -> {
224                 it.targetRotation = targetRotation
225             }
226 
227             is VideoCapture<*> -> {
228                 it.targetRotation = targetRotation
229             }
230         }
231     }
232 }
233 
234 context(CameraSessionContext)
createUseCaseGroupnull235 internal fun createUseCaseGroup(
236     cameraInfo: CameraInfo,
237     initialTransientSettings: TransientSessionSettings,
238     stabilizePreviewMode: Stabilization,
239     stabilizeVideoMode: Stabilization,
240     aspectRatio: AspectRatio,
241     targetFrameRate: Int,
242     dynamicRange: DynamicRange,
243     imageFormat: ImageOutputFormat,
244     useCaseMode: CameraUseCase.UseCaseMode,
245     effect: CameraEffect? = null
246 ): UseCaseGroup {
247     val previewUseCase =
248         createPreviewUseCase(
249             cameraInfo,
250             aspectRatio,
251             stabilizePreviewMode
252         )
253     val imageCaptureUseCase = if (useCaseMode != CameraUseCase.UseCaseMode.VIDEO_ONLY) {
254         createImageUseCase(cameraInfo, aspectRatio, dynamicRange, imageFormat)
255     } else {
256         null
257     }
258     val videoCaptureUseCase = if (useCaseMode != CameraUseCase.UseCaseMode.IMAGE_ONLY) {
259         createVideoUseCase(
260             cameraInfo,
261             aspectRatio,
262             targetFrameRate,
263             stabilizeVideoMode,
264             dynamicRange,
265             backgroundDispatcher
266         )
267     } else {
268         null
269     }
270 
271     imageCaptureUseCase?.let {
272         setFlashModeInternal(
273             imageCapture = imageCaptureUseCase,
274             flashMode = initialTransientSettings.flashMode,
275             isFrontFacing = cameraInfo.appLensFacing == LensFacing.FRONT
276         )
277     }
278 
279     return UseCaseGroup.Builder().apply {
280         Log.d(
281             TAG,
282             "Setting initial device rotation to ${initialTransientSettings.deviceRotation}"
283         )
284         setViewPort(
285             ViewPort.Builder(
286                 aspectRatio.ratio,
287                 // Initialize rotation to Preview's rotation, which comes from Display rotation
288                 previewUseCase.targetRotation
289             ).build()
290         )
291         addUseCase(previewUseCase)
292         imageCaptureUseCase?.let {
293             if (dynamicRange == DynamicRange.SDR ||
294                 imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR
295             ) {
296                 addUseCase(imageCaptureUseCase)
297             }
298         }
299 
300         // Not to bind VideoCapture when Ultra HDR is enabled to keep the app design simple.
301         videoCaptureUseCase?.let {
302             if (imageFormat == ImageOutputFormat.JPEG) {
303                 addUseCase(videoCaptureUseCase)
304             }
305         }
306 
307         effect?.let { addEffect(it) }
308     }.build()
309 }
310 
createImageUseCasenull311 private fun createImageUseCase(
312     cameraInfo: CameraInfo,
313     aspectRatio: AspectRatio,
314     dynamicRange: DynamicRange,
315     imageFormat: ImageOutputFormat
316 ): ImageCapture {
317     val builder = ImageCapture.Builder()
318     builder.setResolutionSelector(
319         getResolutionSelector(cameraInfo.sensorLandscapeRatio, aspectRatio)
320     )
321     if (dynamicRange != DynamicRange.SDR && imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR
322     ) {
323         builder.setOutputFormat(ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR)
324     }
325     return builder.build()
326 }
327 
createVideoUseCasenull328 private fun createVideoUseCase(
329     cameraInfo: CameraInfo,
330     aspectRatio: AspectRatio,
331     targetFrameRate: Int,
332     stabilizeVideoMode: Stabilization,
333     dynamicRange: DynamicRange,
334     backgroundDispatcher: CoroutineDispatcher
335 ): VideoCapture<Recorder> {
336     val sensorLandscapeRatio = cameraInfo.sensorLandscapeRatio
337     val recorder = Recorder.Builder()
338         .setAspectRatio(
339             getAspectRatioForUseCase(sensorLandscapeRatio, aspectRatio)
340         )
341         .setExecutor(backgroundDispatcher.asExecutor()).build()
342     return VideoCapture.Builder(recorder).apply {
343         // set video stabilization
344         if (stabilizeVideoMode == Stabilization.ON) {
345             setVideoStabilizationEnabled(true)
346         }
347         // set target fps
348         if (targetFrameRate != TARGET_FPS_AUTO) {
349             setTargetFrameRate(Range(targetFrameRate, targetFrameRate))
350         }
351 
352         setDynamicRange(dynamicRange.toCXDynamicRange())
353     }.build()
354 }
355 
getAspectRatioForUseCasenull356 private fun getAspectRatioForUseCase(sensorLandscapeRatio: Float, aspectRatio: AspectRatio): Int {
357     return when (aspectRatio) {
358         AspectRatio.THREE_FOUR -> androidx.camera.core.AspectRatio.RATIO_4_3
359         AspectRatio.NINE_SIXTEEN -> androidx.camera.core.AspectRatio.RATIO_16_9
360         else -> {
361             // Choose the aspect ratio which maximizes FOV by being closest to the sensor ratio
362             if (
363                 abs(sensorLandscapeRatio - AspectRatio.NINE_SIXTEEN.landscapeRatio.toFloat()) <
364                 abs(sensorLandscapeRatio - AspectRatio.THREE_FOUR.landscapeRatio.toFloat())
365             ) {
366                 androidx.camera.core.AspectRatio.RATIO_16_9
367             } else {
368                 androidx.camera.core.AspectRatio.RATIO_4_3
369             }
370         }
371     }
372 }
373 
374 context(CameraSessionContext)
createPreviewUseCasenull375 private fun createPreviewUseCase(
376     cameraInfo: CameraInfo,
377     aspectRatio: AspectRatio,
378     stabilizePreviewMode: Stabilization
379 ): Preview = Preview.Builder().apply {
380     updateCameraStateWithCaptureResults(targetCameraInfo = cameraInfo)
381 
382     // set preview stabilization
383     if (stabilizePreviewMode == Stabilization.ON) {
384         setPreviewStabilizationEnabled(true)
385     }
386 
387     setResolutionSelector(
388         getResolutionSelector(cameraInfo.sensorLandscapeRatio, aspectRatio)
389     )
390 }.build()
<lambda>null391     .apply {
392         setSurfaceProvider { surfaceRequest ->
393             surfaceRequests.update { surfaceRequest }
394         }
395     }
396 
getResolutionSelectornull397 private fun getResolutionSelector(
398     sensorLandscapeRatio: Float,
399     aspectRatio: AspectRatio
400 ): ResolutionSelector {
401     val aspectRatioStrategy = when (aspectRatio) {
402         AspectRatio.THREE_FOUR -> AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY
403         AspectRatio.NINE_SIXTEEN -> AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
404         else -> {
405             // Choose the resolution selector strategy which maximizes FOV by being closest
406             // to the sensor aspect ratio
407             if (
408                 abs(sensorLandscapeRatio - AspectRatio.NINE_SIXTEEN.landscapeRatio.toFloat()) <
409                 abs(sensorLandscapeRatio - AspectRatio.THREE_FOUR.landscapeRatio.toFloat())
410             ) {
411                 AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
412             } else {
413                 AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY
414             }
415         }
416     }
417     return ResolutionSelector.Builder().setAspectRatioStrategy(aspectRatioStrategy).build()
418 }
419 
420 context(CameraSessionContext)
setFlashModeInternalnull421 private fun setFlashModeInternal(
422     imageCapture: ImageCapture,
423     flashMode: FlashMode,
424     isFrontFacing: Boolean
425 ) {
426     val isScreenFlashRequired =
427         isFrontFacing && (flashMode == FlashMode.ON || flashMode == FlashMode.AUTO)
428 
429     if (isScreenFlashRequired) {
430         imageCapture.screenFlash = object : ImageCapture.ScreenFlash {
431             override fun apply(
432                 expirationTimeMillis: Long,
433                 listener: ImageCapture.ScreenFlashListener
434             ) {
435                 Log.d(TAG, "ImageCapture.ScreenFlash: apply")
436                 screenFlashEvents.trySend(
437                     CameraUseCase.ScreenFlashEvent(CameraUseCase.ScreenFlashEvent.Type.APPLY_UI) {
438                         listener.onCompleted()
439                     }
440                 )
441             }
442 
443             override fun clear() {
444                 Log.d(TAG, "ImageCapture.ScreenFlash: clear")
445                 screenFlashEvents.trySend(
446                     CameraUseCase.ScreenFlashEvent(CameraUseCase.ScreenFlashEvent.Type.CLEAR_UI) {}
447                 )
448             }
449         }
450     }
451 
452     imageCapture.flashMode = when (flashMode) {
453         FlashMode.OFF -> ImageCapture.FLASH_MODE_OFF // 2
454 
455         FlashMode.ON -> if (isScreenFlashRequired) {
456             ImageCapture.FLASH_MODE_SCREEN // 3
457         } else {
458             ImageCapture.FLASH_MODE_ON // 1
459         }
460 
461         FlashMode.AUTO -> if (isScreenFlashRequired) {
462             ImageCapture.FLASH_MODE_SCREEN // 3
463         } else {
464             ImageCapture.FLASH_MODE_AUTO // 0
465         }
466     }
467     Log.d(TAG, "Set flash mode to: ${imageCapture.flashMode}")
468 }
469 
startVideoRecordingInternalnull470 private suspend fun startVideoRecordingInternal(
471     initialMuted: Boolean,
472     videoCaptureUseCase: VideoCapture<Recorder>,
473     captureTypeSuffix: String,
474     context: Context,
475     videoCaptureUri: Uri?,
476     shouldUseUri: Boolean,
477     onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit
478 ): Recording {
479     Log.d(TAG, "recordVideo")
480     // todo(b/336886716): default setting to enable or disable audio when permission is granted
481 
482     // ok. there is a difference between MUTING and ENABLING audio
483     // audio must be enabled in order to be muted
484     // if the video recording isnt started with audio enabled, you will not be able to unmute it
485     // the toggle should only affect whether or not the audio is muted.
486     // the permission will determine whether or not the audio is enabled.
487     val audioEnabled = checkSelfPermission(
488         context,
489         Manifest.permission.RECORD_AUDIO
490     ) == PackageManager.PERMISSION_GRANTED
491 
492     val pendingRecord = if (shouldUseUri) {
493         val fileOutputOptions = FileOutputOptions.Builder(
494             File(videoCaptureUri!!.path!!)
495         ).build()
496         videoCaptureUseCase.output.prepareRecording(context, fileOutputOptions)
497     } else {
498         val name = "JCA-recording-${Date()}-$captureTypeSuffix.mp4"
499         val contentValues =
500             ContentValues().apply {
501                 put(MediaStore.Video.Media.DISPLAY_NAME, name)
502             }
503         val mediaStoreOutput =
504             MediaStoreOutputOptions.Builder(
505                 context.contentResolver,
506                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI
507             )
508                 .setContentValues(contentValues)
509                 .build()
510         videoCaptureUseCase.output.prepareRecording(context, mediaStoreOutput)
511     }
512     pendingRecord.apply {
513         if (audioEnabled) {
514             withAudioEnabled()
515         }
516     }
517     val callbackExecutor: Executor =
518         (
519             currentCoroutineContext()[ContinuationInterceptor] as?
520                 CoroutineDispatcher
521             )?.asExecutor() ?: ContextCompat.getMainExecutor(context)
522     return pendingRecord.start(callbackExecutor) { onVideoRecordEvent ->
523         Log.d(TAG, onVideoRecordEvent.toString())
524         when (onVideoRecordEvent) {
525             is VideoRecordEvent.Finalize -> {
526                 when (onVideoRecordEvent.error) {
527                     ERROR_NONE ->
528                         onVideoRecord(
529                             CameraUseCase.OnVideoRecordEvent.OnVideoRecorded(
530                                 onVideoRecordEvent.outputResults.outputUri
531                             )
532                         )
533 
534                     else ->
535                         onVideoRecord(
536                             CameraUseCase.OnVideoRecordEvent.OnVideoRecordError(
537                                 onVideoRecordEvent.cause
538                             )
539                         )
540                 }
541             }
542 
543             is VideoRecordEvent.Status -> {
544                 onVideoRecord(
545                     CameraUseCase.OnVideoRecordEvent.OnVideoRecordStatus(
546                         onVideoRecordEvent.recordingStats.audioStats
547                             .audioAmplitude
548                     )
549                 )
550             }
551         }
552     }.apply {
553         mute(initialMuted)
554     }
555 }
556 
runVideoRecordingnull557 private suspend fun runVideoRecording(
558     camera: Camera,
559     videoCapture: VideoCapture<Recorder>,
560     captureTypeSuffix: String,
561     context: Context,
562     transientSettings: StateFlow<TransientSessionSettings?>,
563     videoCaptureUri: Uri?,
564     shouldUseUri: Boolean,
565     onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit
566 ) {
567     var currentSettings = transientSettings.filterNotNull().first()
568 
569     startVideoRecordingInternal(
570         initialMuted = currentSettings.audioMuted,
571         videoCapture,
572         captureTypeSuffix,
573         context,
574         videoCaptureUri,
575         shouldUseUri,
576         onVideoRecord
577     ).use { recording ->
578 
579         fun TransientSessionSettings.isFlashModeOn() = flashMode == FlashMode.ON
580         val isFrontCameraSelector =
581             camera.cameraInfo.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA
582 
583         if (currentSettings.isFlashModeOn()) {
584             if (!isFrontCameraSelector) {
585                 camera.cameraControl.enableTorch(true).await()
586             } else {
587                 Log.d(TAG, "Unable to enable torch for front camera.")
588             }
589         }
590 
591         transientSettings.filterNotNull()
592             .onCompletion {
593                 // Could do some fancier tracking of whether the torch was enabled before
594                 // calling this.
595                 camera.cameraControl.enableTorch(false)
596             }
597             .collectLatest { newTransientSettings ->
598                 if (currentSettings.audioMuted != newTransientSettings.audioMuted) {
599                     recording.mute(newTransientSettings.audioMuted)
600                 }
601                 if (currentSettings.isFlashModeOn() != newTransientSettings.isFlashModeOn()) {
602                     if (!isFrontCameraSelector) {
603                         camera.cameraControl.enableTorch(newTransientSettings.isFlashModeOn())
604                     } else {
605                         Log.d(TAG, "Unable to update torch for front camera.")
606                     }
607                 }
608                 currentSettings = newTransientSettings
609             }
610     }
611 }
612 
613 context(CameraSessionContext)
processFocusMeteringEventsnull614 internal suspend fun processFocusMeteringEvents(cameraControl: CameraControl) {
615     surfaceRequests.map { surfaceRequest ->
616         surfaceRequest?.resolution?.run {
617             Log.d(
618                 TAG,
619                 "Waiting to process focus points for surface with resolution: " +
620                     "$width x $height"
621             )
622             SurfaceOrientedMeteringPointFactory(width.toFloat(), height.toFloat())
623         }
624     }.collectLatest { meteringPointFactory ->
625         for (event in focusMeteringEvents) {
626             meteringPointFactory?.apply {
627                 Log.d(TAG, "tapToFocus, processing event: $event")
628                 val meteringPoint = createPoint(event.x, event.y)
629                 val action = FocusMeteringAction.Builder(meteringPoint).build()
630                 cameraControl.startFocusAndMetering(action)
631             } ?: run {
632                 Log.w(TAG, "Ignoring event due to no SurfaceRequest: $event")
633             }
634         }
635     }
636 }
637 
638 context(CameraSessionContext)
processVideoControlEventsnull639 internal suspend fun processVideoControlEvents(
640     camera: Camera,
641     videoCapture: VideoCapture<Recorder>?,
642     captureTypeSuffix: String
643 ) = coroutineScope {
644     var recordingJob: Job? = null
645 
646     for (event in videoCaptureControlEvents) {
647         when (event) {
648             is VideoCaptureControlEvent.StartRecordingEvent -> {
649                 if (videoCapture == null) {
650                     throw RuntimeException(
651                         "Attempted video recording with null videoCapture"
652                     )
653                 }
654 
655                 recordingJob = launch(start = CoroutineStart.UNDISPATCHED) {
656                     runVideoRecording(
657                         camera,
658                         videoCapture,
659                         captureTypeSuffix,
660                         context,
661                         transientSettings,
662                         event.videoCaptureUri,
663                         event.shouldUseUri,
664                         event.onVideoRecord
665                     )
666                 }
667             }
668 
669             VideoCaptureControlEvent.StopRecordingEvent -> {
670                 recordingJob?.cancel()
671                 recordingJob = null
672             }
673         }
674     }
675 }
676 
677 /**
678  * Applies a CaptureCallback to the provided image capture builder
679  */
680 context(CameraSessionContext)
681 @OptIn(ExperimentalCamera2Interop::class)
Previewnull682 private fun Preview.Builder.updateCameraStateWithCaptureResults(
683     targetCameraInfo: CameraInfo
684 ): Preview.Builder {
685     val isFirstFrameTimestampUpdated = atomic(false)
686     val targetCameraLogicalId = Camera2CameraInfo.from(targetCameraInfo).cameraId
687     Camera2Interop.Extender(this).setSessionCaptureCallback(
688         object : CameraCaptureSession.CaptureCallback() {
689             override fun onCaptureCompleted(
690                 session: CameraCaptureSession,
691                 request: CaptureRequest,
692                 result: TotalCaptureResult
693             ) {
694                 super.onCaptureCompleted(session, request, result)
695                 val logicalCameraId = session.device.id
696                 if (logicalCameraId != targetCameraLogicalId) return
697                 try {
698                     val physicalCameraId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
699                         result.get(CaptureResult.LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID)
700                     } else {
701                         null
702                     }
703                     currentCameraState.update { old ->
704                         if (old.debugInfo.logicalCameraId != logicalCameraId ||
705                             old.debugInfo.physicalCameraId != physicalCameraId
706                         ) {
707                             old.copy(debugInfo = DebugInfo(logicalCameraId, physicalCameraId))
708                         } else {
709                             old
710                         }
711                     }
712                     if (!isFirstFrameTimestampUpdated.value) {
713                         currentCameraState.update { old ->
714                             old.copy(
715                                 sessionFirstFrameTimestamp = SystemClock.elapsedRealtimeNanos()
716                             )
717                         }
718                         isFirstFrameTimestampUpdated.value = true
719                     }
720                 } catch (_: Exception) {
721                 }
722             }
723         }
724     )
725     return this
726 }
727