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