• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.domain.camera
17 
18 import android.Manifest
19 import android.app.Application
20 import android.content.ContentResolver
21 import android.content.ContentValues
22 import android.content.pm.PackageManager
23 import android.net.Uri
24 import android.os.Environment
25 import android.provider.MediaStore
26 import android.util.Log
27 import android.util.Range
28 import android.view.Display
29 import androidx.camera.core.AspectRatio.RATIO_16_9
30 import androidx.camera.core.AspectRatio.RATIO_4_3
31 import androidx.camera.core.AspectRatio.RATIO_DEFAULT
32 import androidx.camera.core.CameraEffect
33 import androidx.camera.core.CameraInfo
34 import androidx.camera.core.CameraSelector
35 import androidx.camera.core.DynamicRange as CXDynamicRange
36 import androidx.camera.core.ImageCapture
37 import androidx.camera.core.ImageCapture.OutputFileOptions
38 import androidx.camera.core.ImageCapture.ScreenFlash
39 import androidx.camera.core.ImageCaptureException
40 import androidx.camera.core.Preview
41 import androidx.camera.core.SurfaceRequest
42 import androidx.camera.core.UseCaseGroup
43 import androidx.camera.core.ViewPort
44 import androidx.camera.core.resolutionselector.AspectRatioStrategy
45 import androidx.camera.core.resolutionselector.ResolutionSelector
46 import androidx.camera.core.takePicture
47 import androidx.camera.lifecycle.ProcessCameraProvider
48 import androidx.camera.lifecycle.awaitInstance
49 import androidx.camera.video.MediaStoreOutputOptions
50 import androidx.camera.video.Recorder
51 import androidx.camera.video.Recording
52 import androidx.camera.video.VideoCapture
53 import androidx.camera.video.VideoRecordEvent
54 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
55 import androidx.core.content.ContextCompat
56 import androidx.core.content.ContextCompat.checkSelfPermission
57 import com.google.jetpackcamera.domain.camera.CameraUseCase.ScreenFlashEvent.Type
58 import com.google.jetpackcamera.domain.camera.effects.SingleSurfaceForcingEffect
59 import com.google.jetpackcamera.settings.SettableConstraintsRepository
60 import com.google.jetpackcamera.settings.SettingsRepository
61 import com.google.jetpackcamera.settings.model.AspectRatio
62 import com.google.jetpackcamera.settings.model.CameraAppSettings
63 import com.google.jetpackcamera.settings.model.CameraConstraints
64 import com.google.jetpackcamera.settings.model.CaptureMode
65 import com.google.jetpackcamera.settings.model.DynamicRange
66 import com.google.jetpackcamera.settings.model.FlashMode
67 import com.google.jetpackcamera.settings.model.LensFacing
68 import com.google.jetpackcamera.settings.model.Stabilization
69 import com.google.jetpackcamera.settings.model.SupportedStabilizationMode
70 import com.google.jetpackcamera.settings.model.SystemConstraints
71 import dagger.hilt.android.scopes.ViewModelScoped
72 import java.io.FileNotFoundException
73 import java.lang.IllegalArgumentException
74 import java.text.SimpleDateFormat
75 import java.util.Calendar
76 import java.util.Date
77 import java.util.Locale
78 import java.util.concurrent.Executor
79 import javax.inject.Inject
80 import kotlin.coroutines.ContinuationInterceptor
81 import kotlin.properties.Delegates
82 import kotlinx.coroutines.CoroutineDispatcher
83 import kotlinx.coroutines.CoroutineScope
84 import kotlinx.coroutines.asExecutor
85 import kotlinx.coroutines.coroutineScope
86 import kotlinx.coroutines.currentCoroutineContext
87 import kotlinx.coroutines.flow.MutableSharedFlow
88 import kotlinx.coroutines.flow.MutableStateFlow
89 import kotlinx.coroutines.flow.StateFlow
90 import kotlinx.coroutines.flow.asSharedFlow
91 import kotlinx.coroutines.flow.asStateFlow
92 import kotlinx.coroutines.flow.collectLatest
93 import kotlinx.coroutines.flow.distinctUntilChanged
94 import kotlinx.coroutines.flow.filterNotNull
95 import kotlinx.coroutines.flow.first
96 import kotlinx.coroutines.flow.map
97 import kotlinx.coroutines.flow.update
98 import kotlinx.coroutines.launch
99 
100 private const val TAG = "CameraXCameraUseCase"
101 const val TARGET_FPS_AUTO = 0
102 const val TARGET_FPS_15 = 15
103 const val TARGET_FPS_30 = 30
104 const val TARGET_FPS_60 = 60
105 
106 /**
107  * CameraX based implementation for [CameraUseCase]
108  */
109 @ViewModelScoped
110 class CameraXCameraUseCase
111 @Inject
112 constructor(
113     private val application: Application,
114     private val coroutineScope: CoroutineScope,
115     private val defaultDispatcher: CoroutineDispatcher,
116     private val settingsRepository: SettingsRepository,
117     private val constraintsRepository: SettableConstraintsRepository
118 ) : CameraUseCase {
119     private lateinit var cameraProvider: ProcessCameraProvider
120 
121     private lateinit var imageCaptureUseCase: ImageCapture
122 
123     private var videoCaptureUseCase: VideoCapture<Recorder>? = null
124     private var recording: Recording? = null
125     private lateinit var captureMode: CaptureMode
126     private lateinit var systemConstraints: SystemConstraints
127     private var disableVideoCapture by Delegates.notNull<Boolean>()
128 
129     private val screenFlashEvents: MutableSharedFlow<CameraUseCase.ScreenFlashEvent> =
130         MutableSharedFlow()
131 
132     private val currentSettings = MutableStateFlow<CameraAppSettings?>(null)
133 
134     override suspend fun initialize(externalImageCapture: Boolean) {
135         this.disableVideoCapture = externalImageCapture
136         cameraProvider = ProcessCameraProvider.awaitInstance(application)
137 
138         // updates values for available cameras
139         val availableCameraLenses =
140             listOf(
141                 LensFacing.FRONT,
142                 LensFacing.BACK
143             ).filter {
144                 cameraProvider.hasCamera(it.toCameraSelector())
145             }
146 
147         // Build and update the system constraints
148         systemConstraints = SystemConstraints(
149             availableLenses = availableCameraLenses,
150             perLensConstraints = buildMap {
151                 val availableCameraInfos = cameraProvider.availableCameraInfos
152                 for (lensFacing in availableCameraLenses) {
153                     val selector = lensFacing.toCameraSelector()
154                     selector.filter(availableCameraInfos).firstOrNull()?.let { camInfo ->
155                         val supportedDynamicRanges =
156                             Recorder.getVideoCapabilities(camInfo).supportedDynamicRanges
157                                 .mapNotNull(CXDynamicRange::toSupportedAppDynamicRange)
158                                 .toSet()
159 
160                         val supportedStabilizationModes = buildSet {
161                             if (isPreviewStabilizationSupported(camInfo)) {
162                                 add(SupportedStabilizationMode.ON)
163                             }
164 
165                             if (isVideoStabilizationSupported(camInfo)) {
166                                 add(SupportedStabilizationMode.HIGH_QUALITY)
167                             }
168                         }
169 
170                         val supportedFixedFrameRates = getSupportedFrameRates(camInfo)
171 
172                         put(
173                             lensFacing,
174                             CameraConstraints(
175                                 supportedStabilizationModes = supportedStabilizationModes,
176                                 supportedFixedFrameRates = supportedFixedFrameRates,
177                                 supportedDynamicRanges = supportedDynamicRanges
178                             )
179                         )
180                     }
181                 }
182             }
183         )
184 
185         constraintsRepository.updateSystemConstraints(systemConstraints)
186 
187         currentSettings.value =
188             settingsRepository.defaultCameraAppSettings.first()
189                 .tryApplyDynamicRangeConstraints()
190                 .tryApplyAspectRatioForExternalCapture(externalImageCapture)
191 
192         imageCaptureUseCase = ImageCapture.Builder()
193             .setResolutionSelector(
194                 getResolutionSelector(
195                     settingsRepository.defaultCameraAppSettings.first().aspectRatio
196                 )
197             ).build()
198     }
199 
200     /**
201      * Returns the union of supported stabilization modes for a device's cameras
202      */
203     private fun getDeviceSupportedStabilizations(): Set<SupportedStabilizationMode> {
204         val deviceSupportedStabilizationModes = mutableSetOf<SupportedStabilizationMode>()
205 
206         cameraProvider.availableCameraInfos.forEach { cameraInfo ->
207             if (isPreviewStabilizationSupported(cameraInfo)) {
208                 deviceSupportedStabilizationModes.add(SupportedStabilizationMode.ON)
209             }
210             if (isVideoStabilizationSupported(cameraInfo)) {
211                 deviceSupportedStabilizationModes.add(SupportedStabilizationMode.HIGH_QUALITY)
212             }
213         }
214         return deviceSupportedStabilizationModes
215     }
216 
217     /**
218      * Camera settings that persist as long as a camera is running.
219      *
220      * Any change in these settings will require calling [ProcessCameraProvider.runWith] with
221      * updates [CameraSelector] and/or [UseCaseGroup]
222      */
223     private data class PerpetualSessionSettings(
224         val cameraSelector: CameraSelector,
225         val aspectRatio: AspectRatio,
226         val captureMode: CaptureMode,
227         val targetFrameRate: Int,
228         val stabilizePreviewMode: Stabilization,
229         val stabilizeVideoMode: Stabilization,
230         val dynamicRange: DynamicRange
231     )
232 
233     /**
234      * Camera settings that can change while the camera is running.
235      *
236      * Any changes in these settings can be applied either directly to use cases via their
237      * setter methods or to [androidx.camera.core.CameraControl].
238      * The use cases typically will not need to be re-bound.
239      */
240     private data class TransientSessionSettings(
241         val flashMode: FlashMode,
242         val zoomScale: Float
243     )
244 
245     override suspend fun runCamera() = coroutineScope {
246         Log.d(TAG, "runCamera")
247 
248         val transientSettings = MutableStateFlow<TransientSessionSettings?>(null)
249         currentSettings
250             .filterNotNull()
251             .map { currentCameraSettings ->
252                 transientSettings.value = TransientSessionSettings(
253                     flashMode = currentCameraSettings.flashMode,
254                     zoomScale = currentCameraSettings.zoomScale
255                 )
256 
257                 val cameraSelector = when (currentCameraSettings.cameraLensFacing) {
258                     LensFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA
259                     LensFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA
260                 }
261 
262                 PerpetualSessionSettings(
263                     cameraSelector = cameraSelector,
264                     aspectRatio = currentCameraSettings.aspectRatio,
265                     captureMode = currentCameraSettings.captureMode,
266                     targetFrameRate = currentCameraSettings.targetFrameRate,
267                     stabilizePreviewMode = currentCameraSettings.previewStabilization,
268                     stabilizeVideoMode = currentCameraSettings.videoCaptureStabilization,
269                     dynamicRange = currentCameraSettings.dynamicRange
270                 )
271             }.distinctUntilChanged()
272             .collectLatest { sessionSettings ->
273                 Log.d(TAG, "Starting new camera session")
274                 val cameraInfo = sessionSettings.cameraSelector.filter(
275                     cameraProvider.availableCameraInfos
276                 ).first()
277 
278                 val lensFacing = sessionSettings.cameraSelector.toAppLensFacing()
279                 val cameraConstraints = checkNotNull(
280                     systemConstraints.perLensConstraints[lensFacing]
281                 ) {
282                     "Unable to retrieve CameraConstraints for $lensFacing. " +
283                         "Was the use case initialized?"
284                 }
285 
286                 val initialTransientSettings = transientSettings
287                     .filterNotNull()
288                     .first()
289 
290                 val useCaseGroup = createUseCaseGroup(
291                     sessionSettings,
292                     initialTransientSettings,
293                     cameraConstraints.supportedStabilizationModes,
294                     effect = when (sessionSettings.captureMode) {
295                         CaptureMode.SINGLE_STREAM -> SingleSurfaceForcingEffect(coroutineScope)
296                         CaptureMode.MULTI_STREAM -> null
297                     }
298                 )
299 
300                 var prevTransientSettings = initialTransientSettings
301                 cameraProvider.runWith(sessionSettings.cameraSelector, useCaseGroup) { camera ->
302                     Log.d(TAG, "Camera session started")
303                     transientSettings.filterNotNull().collectLatest { newTransientSettings ->
304                         // Apply camera control settings
305                         if (prevTransientSettings.zoomScale != newTransientSettings.zoomScale) {
306                             cameraInfo.zoomState.value?.let { zoomState ->
307                                 val finalScale =
308                                     (zoomState.zoomRatio * newTransientSettings.zoomScale).coerceIn(
309                                         zoomState.minZoomRatio,
310                                         zoomState.maxZoomRatio
311                                     )
312                                 camera.cameraControl.setZoomRatio(finalScale)
313                                 _zoomScale.value = finalScale
314                             }
315                         }
316 
317                         if (prevTransientSettings.flashMode != newTransientSettings.flashMode) {
318                             setFlashModeInternal(
319                                 flashMode = newTransientSettings.flashMode,
320                                 isFrontFacing = sessionSettings.cameraSelector
321                                     == CameraSelector.DEFAULT_FRONT_CAMERA
322                             )
323                         }
324 
325                         prevTransientSettings = newTransientSettings
326                     }
327                 }
328             }
329     }
330 
331     override suspend fun takePicture(onCaptureStarted: (() -> Unit)) {
332         try {
333             val imageProxy = imageCaptureUseCase.takePicture(onCaptureStarted)
334             Log.d(TAG, "onCaptureSuccess")
335             imageProxy.close()
336         } catch (exception: Exception) {
337             Log.d(TAG, "takePicture onError: $exception")
338             throw exception
339         }
340     }
341 
342     // TODO(b/319733374): Return bitmap for external mediastore capture without URI
343     override suspend fun takePicture(
344         onCaptureStarted: (() -> Unit),
345         contentResolver: ContentResolver,
346         imageCaptureUri: Uri?,
347         ignoreUri: Boolean
348     ): ImageCapture.OutputFileResults {
349         val eligibleContentValues = getEligibleContentValues()
350         val outputFileOptions: OutputFileOptions
351         if (ignoreUri) {
352             val formatter = SimpleDateFormat(
353                 "yyyy-MM-dd-HH-mm-ss-SSS",
354                 Locale.US
355             )
356             val filename = "JCA-${formatter.format(Calendar.getInstance().time)}.jpg"
357             val contentValues = ContentValues()
358             contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
359             contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
360             outputFileOptions = OutputFileOptions.Builder(
361                 contentResolver,
362                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
363                 contentValues
364             ).build()
365         } else if (imageCaptureUri == null) {
366             val e = RuntimeException("Null Uri is provided.")
367             Log.d(TAG, "takePicture onError: $e")
368             throw e
369         } else {
370             try {
371                 val outputStream = contentResolver.openOutputStream(imageCaptureUri)
372                 if (outputStream != null) {
373                     outputFileOptions =
374                         OutputFileOptions.Builder(
375                             contentResolver.openOutputStream(imageCaptureUri)!!
376                         ).build()
377                 } else {
378                     val e = RuntimeException("Provider recently crashed.")
379                     Log.d(TAG, "takePicture onError: $e")
380                     throw e
381                 }
382             } catch (e: FileNotFoundException) {
383                 Log.d(TAG, "takePicture onError: $e")
384                 throw e
385             }
386         }
387         try {
388             val outputFileResults = imageCaptureUseCase.takePicture(
389                 outputFileOptions,
390                 onCaptureStarted
391             )
392             val relativePath =
393                 eligibleContentValues.getAsString(MediaStore.Images.Media.RELATIVE_PATH)
394             val displayName = eligibleContentValues.getAsString(
395                 MediaStore.Images.Media.DISPLAY_NAME
396             )
397             Log.d(TAG, "Saved image to $relativePath/$displayName")
398             return outputFileResults
399         } catch (exception: ImageCaptureException) {
400             Log.d(TAG, "takePicture onError: $exception")
401             throw exception
402         }
403     }
404 
405     private fun getEligibleContentValues(): ContentValues {
406         val eligibleContentValues = ContentValues()
407         eligibleContentValues.put(
408             MediaStore.Images.Media.DISPLAY_NAME,
409             Calendar.getInstance().time.toString()
410         )
411         eligibleContentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
412         eligibleContentValues.put(
413             MediaStore.Images.Media.RELATIVE_PATH,
414             Environment.DIRECTORY_PICTURES
415         )
416         return eligibleContentValues
417     }
418 
419     override suspend fun startVideoRecording(
420         onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit
421     ) {
422         if (videoCaptureUseCase == null) {
423             throw RuntimeException("Attempted video recording with null videoCapture use case")
424         }
425         Log.d(TAG, "recordVideo")
426         // todo(b/336886716): default setting to enable or disable audio when permission is granted
427         // todo(b/336888844): mute/unmute audio while recording is active
428         val audioEnabled = (
429             checkSelfPermission(
430                 this.application.baseContext,
431                 Manifest.permission.RECORD_AUDIO
432             )
433                 == PackageManager.PERMISSION_GRANTED
434             )
435         val captureTypeString =
436             when (captureMode) {
437                 CaptureMode.MULTI_STREAM -> "MultiStream"
438                 CaptureMode.SINGLE_STREAM -> "SingleStream"
439             }
440         val name = "JCA-recording-${Date()}-$captureTypeString.mp4"
441         val contentValues =
442             ContentValues().apply {
443                 put(MediaStore.Video.Media.DISPLAY_NAME, name)
444             }
445 
446         val mediaStoreOutput =
447             MediaStoreOutputOptions.Builder(
448                 application.contentResolver,
449                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI
450             )
451                 .setContentValues(contentValues)
452                 .build()
453 
454         val callbackExecutor: Executor =
455             (
456                 currentCoroutineContext()[ContinuationInterceptor] as?
457                     CoroutineDispatcher
458                 )?.asExecutor() ?: ContextCompat.getMainExecutor(application)
459         recording =
460             videoCaptureUseCase!!.output
461                 .prepareRecording(application, mediaStoreOutput)
462                 .apply { if (audioEnabled) withAudioEnabled() }
463                 .start(callbackExecutor) { onVideoRecordEvent ->
464                     run {
465                         Log.d(TAG, onVideoRecordEvent.toString())
466                         when (onVideoRecordEvent) {
467                             is VideoRecordEvent.Finalize -> {
468                                 when (onVideoRecordEvent.error) {
469                                     ERROR_NONE ->
470                                         onVideoRecord(
471                                             CameraUseCase.OnVideoRecordEvent.OnVideoRecorded
472                                         )
473                                     else ->
474                                         onVideoRecord(
475                                             CameraUseCase.OnVideoRecordEvent.OnVideoRecordError
476                                         )
477                                 }
478                             }
479                             is VideoRecordEvent.Status -> {
480                                 onVideoRecord(
481                                     CameraUseCase.OnVideoRecordEvent.OnVideoRecordStatus(
482                                         onVideoRecordEvent.recordingStats.audioStats.audioAmplitude
483                                     )
484                                 )
485                             }
486                         }
487                     }
488                 }
489     }
490 
491     override fun stopVideoRecording() {
492         Log.d(TAG, "stopRecording")
493         recording?.stop()
494     }
495 
496     override fun setZoomScale(scale: Float) {
497         currentSettings.update { old ->
498             old?.copy(zoomScale = scale)
499         }
500     }
501 
502     // Could be improved by setting initial value only when camera is initialized
503     private val _zoomScale = MutableStateFlow(1f)
504     override fun getZoomScale(): StateFlow<Float> = _zoomScale.asStateFlow()
505 
506     private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
507     override fun getSurfaceRequest(): StateFlow<SurfaceRequest?> = _surfaceRequest.asStateFlow()
508 
509     // Sets the camera to the designated lensFacing direction
510     override suspend fun setLensFacing(lensFacing: LensFacing) {
511         currentSettings.update { old ->
512             if (systemConstraints.availableLenses.contains(lensFacing)) {
513                 old?.copy(cameraLensFacing = lensFacing)
514                     ?.tryApplyDynamicRangeConstraints()
515             } else {
516                 old
517             }
518         }
519     }
520 
521     private fun CameraAppSettings.tryApplyDynamicRangeConstraints(): CameraAppSettings {
522         return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints ->
523             with(constraints.supportedDynamicRanges) {
524                 val newDynamicRange = if (contains(dynamicRange)) {
525                     dynamicRange
526                 } else {
527                     DynamicRange.SDR
528                 }
529 
530                 this@tryApplyDynamicRangeConstraints.copy(
531                     dynamicRange = newDynamicRange
532                 )
533             }
534         } ?: this
535     }
536 
537     private fun CameraAppSettings.tryApplyAspectRatioForExternalCapture(
538         externalImageCapture: Boolean
539     ): CameraAppSettings {
540         if (externalImageCapture) {
541             return this.copy(aspectRatio = AspectRatio.THREE_FOUR)
542         }
543         return this
544     }
545 
546     override fun tapToFocus(
547         display: Display,
548         surfaceWidth: Int,
549         surfaceHeight: Int,
550         x: Float,
551         y: Float
552     ) {
553         // TODO(tm):Convert API to use SurfaceOrientedMeteringPointFactory and
554         // use a Channel to get result of FocusMeteringAction
555     }
556 
557     override fun getScreenFlashEvents() = screenFlashEvents.asSharedFlow()
558     override fun getCurrentSettings() = currentSettings.asStateFlow()
559 
560     override fun setFlashMode(flashMode: FlashMode) {
561         currentSettings.update { old ->
562             old?.copy(flashMode = flashMode)
563         }
564     }
565 
566     private fun setFlashModeInternal(flashMode: FlashMode, isFrontFacing: Boolean) {
567         val isScreenFlashRequired =
568             isFrontFacing && (flashMode == FlashMode.ON || flashMode == FlashMode.AUTO)
569 
570         if (isScreenFlashRequired) {
571             imageCaptureUseCase.screenFlash = object : ScreenFlash {
572                 override fun apply(
573                     expirationTimeMillis: Long,
574                     listener: ImageCapture.ScreenFlashListener
575                 ) {
576                     Log.d(TAG, "ImageCapture.ScreenFlash: apply")
577                     coroutineScope.launch {
578                         screenFlashEvents.emit(
579                             CameraUseCase.ScreenFlashEvent(Type.APPLY_UI) {
580                                 listener.onCompleted()
581                             }
582                         )
583                     }
584                 }
585 
586                 override fun clear() {
587                     Log.d(TAG, "ImageCapture.ScreenFlash: clear")
588                     coroutineScope.launch {
589                         screenFlashEvents.emit(
590                             CameraUseCase.ScreenFlashEvent(Type.CLEAR_UI) {}
591                         )
592                     }
593                 }
594             }
595         }
596 
597         imageCaptureUseCase.flashMode = when (flashMode) {
598             FlashMode.OFF -> ImageCapture.FLASH_MODE_OFF // 2
599 
600             FlashMode.ON -> if (isScreenFlashRequired) {
601                 ImageCapture.FLASH_MODE_SCREEN // 3
602             } else {
603                 ImageCapture.FLASH_MODE_ON // 1
604             }
605 
606             FlashMode.AUTO -> if (isScreenFlashRequired) {
607                 ImageCapture.FLASH_MODE_SCREEN // 3
608             } else {
609                 ImageCapture.FLASH_MODE_AUTO // 0
610             }
611         }
612         Log.d(TAG, "Set flash mode to: ${imageCaptureUseCase.flashMode}")
613     }
614 
615     override fun isScreenFlashEnabled() =
616         imageCaptureUseCase.flashMode == ImageCapture.FLASH_MODE_SCREEN &&
617             imageCaptureUseCase.screenFlash != null
618 
619     override suspend fun setAspectRatio(aspectRatio: AspectRatio) {
620         currentSettings.update { old ->
621             old?.copy(aspectRatio = aspectRatio)
622         }
623     }
624 
625     override suspend fun setCaptureMode(captureMode: CaptureMode) {
626         currentSettings.update { old ->
627             old?.copy(captureMode = captureMode)
628         }
629     }
630 
631     private fun createUseCaseGroup(
632         sessionSettings: PerpetualSessionSettings,
633         initialTransientSettings: TransientSessionSettings,
634         supportedStabilizationModes: Set<SupportedStabilizationMode>,
635         effect: CameraEffect? = null
636     ): UseCaseGroup {
637         val previewUseCase = createPreviewUseCase(sessionSettings, supportedStabilizationModes)
638         if (!disableVideoCapture) {
639             videoCaptureUseCase = createVideoUseCase(sessionSettings, supportedStabilizationModes)
640         }
641 
642         setFlashModeInternal(
643             flashMode = initialTransientSettings.flashMode,
644             isFrontFacing = sessionSettings.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA
645         )
646         imageCaptureUseCase = ImageCapture.Builder()
647             .setResolutionSelector(getResolutionSelector(sessionSettings.aspectRatio)).build()
648 
649         return UseCaseGroup.Builder().apply {
650             setViewPort(
651                 ViewPort.Builder(
652                     sessionSettings.aspectRatio.ratio,
653                     previewUseCase.targetRotation
654                 ).build()
655             )
656             addUseCase(previewUseCase)
657             if (sessionSettings.dynamicRange == DynamicRange.SDR) {
658                 addUseCase(imageCaptureUseCase)
659             }
660             if (videoCaptureUseCase != null) {
661                 addUseCase(videoCaptureUseCase!!)
662             }
663 
664 //            effect?.let { addEffect(it) }
665 
666             captureMode = sessionSettings.captureMode
667         }.build()
668     }
669     override suspend fun setDynamicRange(dynamicRange: DynamicRange) {
670         currentSettings.update { old ->
671             old?.copy(dynamicRange = dynamicRange)
672         }
673     }
674 
675     private fun createVideoUseCase(
676         sessionSettings: PerpetualSessionSettings,
677         supportedStabilizationMode: Set<SupportedStabilizationMode>
678     ): VideoCapture<Recorder> {
679         val recorder = Recorder.Builder()
680             .setAspectRatio(getAspectRatioForUseCase(sessionSettings.aspectRatio))
681             .setExecutor(defaultDispatcher.asExecutor()).build()
682         return VideoCapture.Builder(recorder).apply {
683             // set video stabilization
684             if (shouldVideoBeStabilized(sessionSettings, supportedStabilizationMode)
685             ) {
686                 setVideoStabilizationEnabled(true)
687             }
688             // set target fps
689             if (sessionSettings.targetFrameRate != TARGET_FPS_AUTO) {
690                 setTargetFrameRate(
691                     Range(sessionSettings.targetFrameRate, sessionSettings.targetFrameRate)
692                 )
693             }
694 
695             setDynamicRange(sessionSettings.dynamicRange.toCXDynamicRange())
696         }.build()
697     }
698 
699     private fun getAspectRatioForUseCase(aspectRatio: AspectRatio): Int {
700         return when (aspectRatio) {
701             AspectRatio.THREE_FOUR -> RATIO_4_3
702             AspectRatio.NINE_SIXTEEN -> RATIO_16_9
703             else -> RATIO_DEFAULT
704         }
705     }
706 
707     private fun shouldVideoBeStabilized(
708         sessionSettings: PerpetualSessionSettings,
709         supportedStabilizationModes: Set<SupportedStabilizationMode>
710     ): Boolean {
711         // video is on and target fps is not 60
712         return (sessionSettings.targetFrameRate != TARGET_FPS_60) &&
713             (supportedStabilizationModes.contains(SupportedStabilizationMode.HIGH_QUALITY)) &&
714             // high quality (video only) selected
715             (
716                 sessionSettings.stabilizeVideoMode == Stabilization.ON &&
717                     sessionSettings.stabilizePreviewMode == Stabilization.UNDEFINED
718                 )
719     }
720 
721     private fun createPreviewUseCase(
722         sessionSettings: PerpetualSessionSettings,
723         supportedStabilizationModes: Set<SupportedStabilizationMode>
724     ): Preview {
725         val previewUseCaseBuilder = Preview.Builder()
726         // set preview stabilization
727         if (shouldPreviewBeStabilized(sessionSettings, supportedStabilizationModes)) {
728             previewUseCaseBuilder.setPreviewStabilizationEnabled(true)
729         }
730 
731         previewUseCaseBuilder.setResolutionSelector(
732             getResolutionSelector(sessionSettings.aspectRatio)
733         )
734 
735         return previewUseCaseBuilder.build().apply {
736             setSurfaceProvider { surfaceRequest ->
737                 _surfaceRequest.value = surfaceRequest
738             }
739         }
740     }
741 
742     private fun getResolutionSelector(aspectRatio: AspectRatio): ResolutionSelector {
743         val aspectRatioStrategy = when (aspectRatio) {
744             AspectRatio.THREE_FOUR -> AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY
745             AspectRatio.NINE_SIXTEEN -> AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
746             else -> AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
747         }
748         return ResolutionSelector.Builder().setAspectRatioStrategy(aspectRatioStrategy).build()
749     }
750 
751     private fun shouldPreviewBeStabilized(
752         sessionSettings: PerpetualSessionSettings,
753         supportedStabilizationModes: Set<SupportedStabilizationMode>
754     ): Boolean {
755         // only supported if target fps is 30 or none
756         return (
757             when (sessionSettings.targetFrameRate) {
758                 TARGET_FPS_AUTO, TARGET_FPS_30 -> true
759                 else -> false
760             }
761             ) &&
762             (
763                 supportedStabilizationModes.contains(SupportedStabilizationMode.ON) &&
764                     sessionSettings.stabilizePreviewMode == Stabilization.ON
765                 )
766     }
767 
768     companion object {
769         private val FIXED_FRAME_RATES = setOf(TARGET_FPS_15, TARGET_FPS_30, TARGET_FPS_60)
770 
771         /**
772          * Checks if preview stabilization is supported by the device.
773          *
774          */
775         private fun isPreviewStabilizationSupported(cameraInfo: CameraInfo): Boolean {
776             return Preview.getPreviewCapabilities(cameraInfo).isStabilizationSupported
777         }
778 
779         /**
780          * Checks if video stabilization is supported by the device.
781          *
782          */
783         private fun isVideoStabilizationSupported(cameraInfo: CameraInfo): Boolean {
784             return Recorder.getVideoCapabilities(cameraInfo).isStabilizationSupported
785         }
786 
787         private fun getSupportedFrameRates(camInfo: CameraInfo): Set<Int> {
788             return buildSet {
789                 camInfo.supportedFrameRateRanges.forEach { e ->
790                     if (e.upper == e.lower && FIXED_FRAME_RATES.contains(e.upper)) {
791                         add(e.upper)
792                     }
793                 }
794             }
795         }
796     }
797 }
798 
toSupportedAppDynamicRangenull799 private fun CXDynamicRange.toSupportedAppDynamicRange(): DynamicRange? {
800     return when (this) {
801         CXDynamicRange.SDR -> DynamicRange.SDR
802         CXDynamicRange.HLG_10_BIT -> DynamicRange.HLG10
803         // All other dynamic ranges unsupported. Return null.
804         else -> null
805     }
806 }
807 
toCXDynamicRangenull808 private fun DynamicRange.toCXDynamicRange(): CXDynamicRange {
809     return when (this) {
810         DynamicRange.SDR -> CXDynamicRange.SDR
811         DynamicRange.HLG10 -> CXDynamicRange.HLG_10_BIT
812     }
813 }
814 
toCameraSelectornull815 private fun LensFacing.toCameraSelector(): CameraSelector = when (this) {
816     LensFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA
817     LensFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA
818 }
819 
CameraSelectornull820 private fun CameraSelector.toAppLensFacing(): LensFacing = when (this) {
821     CameraSelector.DEFAULT_FRONT_CAMERA -> LensFacing.FRONT
822     CameraSelector.DEFAULT_BACK_CAMERA -> LensFacing.BACK
823     else -> throw IllegalArgumentException(
824         "Unknown CameraSelector -> LensFacing mapping. [CameraSelector: $this]"
825     )
826 }
827 
828