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.core.camera
17 
18 import android.app.Application
19 import android.content.ContentResolver
20 import android.content.ContentValues
21 import android.net.Uri
22 import android.os.Build
23 import android.os.Environment
24 import android.os.Environment.DIRECTORY_DOCUMENTS
25 import android.provider.MediaStore
26 import android.util.Log
27 import androidx.camera.core.CameraInfo
28 import androidx.camera.core.CameraSelector
29 import androidx.camera.core.DynamicRange as CXDynamicRange
30 import androidx.camera.core.ImageCapture
31 import androidx.camera.core.ImageCapture.OutputFileOptions
32 import androidx.camera.core.ImageCaptureException
33 import androidx.camera.core.SurfaceRequest
34 import androidx.camera.core.takePicture
35 import androidx.camera.lifecycle.ProcessCameraProvider
36 import androidx.camera.lifecycle.awaitInstance
37 import androidx.camera.video.Recorder
38 import com.google.jetpackcamera.core.camera.DebugCameraInfoUtil.getAllCamerasPropertiesJSONArray
39 import com.google.jetpackcamera.core.camera.DebugCameraInfoUtil.writeFileExternalStorage
40 import com.google.jetpackcamera.core.common.DefaultDispatcher
41 import com.google.jetpackcamera.core.common.IODispatcher
42 import com.google.jetpackcamera.settings.SettableConstraintsRepository
43 import com.google.jetpackcamera.settings.model.AspectRatio
44 import com.google.jetpackcamera.settings.model.CameraAppSettings
45 import com.google.jetpackcamera.settings.model.CameraConstraints
46 import com.google.jetpackcamera.settings.model.CaptureMode
47 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
48 import com.google.jetpackcamera.settings.model.DeviceRotation
49 import com.google.jetpackcamera.settings.model.DynamicRange
50 import com.google.jetpackcamera.settings.model.FlashMode
51 import com.google.jetpackcamera.settings.model.ImageOutputFormat
52 import com.google.jetpackcamera.settings.model.LensFacing
53 import com.google.jetpackcamera.settings.model.LowLightBoost
54 import com.google.jetpackcamera.settings.model.Stabilization
55 import com.google.jetpackcamera.settings.model.SupportedStabilizationMode
56 import com.google.jetpackcamera.settings.model.SystemConstraints
57 import dagger.hilt.android.scopes.ViewModelScoped
58 import java.io.File
59 import java.io.FileNotFoundException
60 import java.text.SimpleDateFormat
61 import java.util.Calendar
62 import java.util.Locale
63 import javax.inject.Inject
64 import kotlin.properties.Delegates
65 import kotlinx.coroutines.CoroutineDispatcher
66 import kotlinx.coroutines.channels.Channel
67 import kotlinx.coroutines.channels.trySendBlocking
68 import kotlinx.coroutines.coroutineScope
69 import kotlinx.coroutines.flow.MutableStateFlow
70 import kotlinx.coroutines.flow.StateFlow
71 import kotlinx.coroutines.flow.asStateFlow
72 import kotlinx.coroutines.flow.collectLatest
73 import kotlinx.coroutines.flow.distinctUntilChanged
74 import kotlinx.coroutines.flow.filterNotNull
75 import kotlinx.coroutines.flow.map
76 import kotlinx.coroutines.flow.update
77 import kotlinx.coroutines.withContext
78 
79 private const val TAG = "CameraXCameraUseCase"
80 const val TARGET_FPS_AUTO = 0
81 const val TARGET_FPS_15 = 15
82 const val TARGET_FPS_30 = 30
83 const val TARGET_FPS_60 = 60
84 
85 /**
86  * CameraX based implementation for [CameraUseCase]
87  */
88 @ViewModelScoped
89 class CameraXCameraUseCase
90 @Inject
91 constructor(
92     private val application: Application,
93     @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
94     @IODispatcher private val iODispatcher: CoroutineDispatcher,
95     private val constraintsRepository: SettableConstraintsRepository
96 ) : CameraUseCase {
97     private lateinit var cameraProvider: ProcessCameraProvider
98 
99     private var imageCaptureUseCase: ImageCapture? = null
100 
101     private lateinit var systemConstraints: SystemConstraints
102     private var useCaseMode by Delegates.notNull<CameraUseCase.UseCaseMode>()
103 
104     private val screenFlashEvents: Channel<CameraUseCase.ScreenFlashEvent> =
105         Channel(capacity = Channel.UNLIMITED)
106     private val focusMeteringEvents =
107         Channel<CameraEvent.FocusMeteringEvent>(capacity = Channel.CONFLATED)
108     private val videoCaptureControlEvents = Channel<VideoCaptureControlEvent>()
109 
110     private val currentSettings = MutableStateFlow<CameraAppSettings?>(null)
111 
112     // Could be improved by setting initial value only when camera is initialized
113     private val _currentCameraState = MutableStateFlow(CameraState())
114     override fun getCurrentCameraState(): StateFlow<CameraState> = _currentCameraState.asStateFlow()
115 
116     private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
117     override fun getSurfaceRequest(): StateFlow<SurfaceRequest?> = _surfaceRequest.asStateFlow()
118 
119     override suspend fun initialize(
120         cameraAppSettings: CameraAppSettings,
121         useCaseMode: CameraUseCase.UseCaseMode,
122         isDebugMode: Boolean
123     ) {
124         this.useCaseMode = useCaseMode
125         cameraProvider = ProcessCameraProvider.awaitInstance(application)
126 
127         // updates values for available cameras
128         val availableCameraLenses =
129             listOf(
130                 LensFacing.FRONT,
131                 LensFacing.BACK
132             ).filter {
133                 cameraProvider.hasCamera(it.toCameraSelector())
134             }
135 
136         // Build and update the system constraints
137         systemConstraints = SystemConstraints(
138             availableLenses = availableCameraLenses,
139             concurrentCamerasSupported = cameraProvider.availableConcurrentCameraInfos.any {
140                 it.map { cameraInfo -> cameraInfo.cameraSelector.toAppLensFacing() }
141                     .toSet() == setOf(LensFacing.FRONT, LensFacing.BACK)
142             },
143             perLensConstraints = buildMap {
144                 val availableCameraInfos = cameraProvider.availableCameraInfos
145                 for (lensFacing in availableCameraLenses) {
146                     val selector = lensFacing.toCameraSelector()
147                     selector.filter(availableCameraInfos).firstOrNull()?.let { camInfo ->
148                         val supportedDynamicRanges =
149                             Recorder.getVideoCapabilities(camInfo).supportedDynamicRanges
150                                 .mapNotNull(CXDynamicRange::toSupportedAppDynamicRange)
151                                 .toSet()
152 
153                         val supportedStabilizationModes = buildSet {
154                             if (camInfo.isPreviewStabilizationSupported) {
155                                 add(SupportedStabilizationMode.ON)
156                             }
157 
158                             if (camInfo.isVideoStabilizationSupported) {
159                                 add(SupportedStabilizationMode.HIGH_QUALITY)
160                             }
161                         }
162 
163                         val supportedFixedFrameRates =
164                             camInfo.filterSupportedFixedFrameRates(FIXED_FRAME_RATES)
165                         val supportedImageFormats = camInfo.supportedImageFormats
166                         val hasFlashUnit = camInfo.hasFlashUnit()
167 
168                         put(
169                             lensFacing,
170                             CameraConstraints(
171                                 supportedStabilizationModes = supportedStabilizationModes,
172                                 supportedFixedFrameRates = supportedFixedFrameRates,
173                                 supportedDynamicRanges = supportedDynamicRanges,
174                                 supportedImageFormatsMap = mapOf(
175                                     // Only JPEG is supported in single-stream mode, since
176                                     // single-stream mode uses CameraEffect, which does not support
177                                     // Ultra HDR now.
178                                     Pair(CaptureMode.SINGLE_STREAM, setOf(ImageOutputFormat.JPEG)),
179                                     Pair(CaptureMode.MULTI_STREAM, supportedImageFormats)
180                                 ),
181                                 hasFlashUnit = hasFlashUnit
182                             )
183                         )
184                     }
185                 }
186             }
187         )
188 
189         constraintsRepository.updateSystemConstraints(systemConstraints)
190 
191         currentSettings.value =
192             cameraAppSettings
193                 .tryApplyDynamicRangeConstraints()
194                 .tryApplyAspectRatioForExternalCapture(this.useCaseMode)
195                 .tryApplyImageFormatConstraints()
196                 .tryApplyFrameRateConstraints()
197                 .tryApplyStabilizationConstraints()
198                 .tryApplyConcurrentCameraModeConstraints()
199         if (isDebugMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
200             withContext(iODispatcher) {
201                 val cameraProperties =
202                     getAllCamerasPropertiesJSONArray(cameraProvider.availableCameraInfos).toString()
203                 val file = File(
204                     Environment.getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS),
205                     "JCACameraProperties.json"
206                 )
207                 writeFileExternalStorage(file, cameraProperties)
208                 Log.d(TAG, "JCACameraProperties written to ${file.path}. \n$cameraProperties")
209             }
210         }
211     }
212 
213     override suspend fun runCamera() = coroutineScope {
214         Log.d(TAG, "runCamera")
215 
216         val transientSettings = MutableStateFlow<TransientSessionSettings?>(null)
217         currentSettings
218             .filterNotNull()
219             .map { currentCameraSettings ->
220                 transientSettings.value = TransientSessionSettings(
221                     audioMuted = currentCameraSettings.audioMuted,
222                     deviceRotation = currentCameraSettings.deviceRotation,
223                     flashMode = currentCameraSettings.flashMode,
224                     zoomScale = currentCameraSettings.zoomScale
225                 )
226 
227                 when (currentCameraSettings.concurrentCameraMode) {
228                     ConcurrentCameraMode.OFF -> {
229                         val cameraSelector = when (currentCameraSettings.cameraLensFacing) {
230                             LensFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA
231                             LensFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA
232                         }
233 
234                         PerpetualSessionSettings.SingleCamera(
235                             cameraInfo = cameraProvider.getCameraInfo(cameraSelector),
236                             aspectRatio = currentCameraSettings.aspectRatio,
237                             captureMode = currentCameraSettings.captureMode,
238                             targetFrameRate = currentCameraSettings.targetFrameRate,
239                             stabilizePreviewMode = currentCameraSettings.previewStabilization,
240                             stabilizeVideoMode = currentCameraSettings.videoCaptureStabilization,
241                             dynamicRange = currentCameraSettings.dynamicRange,
242                             imageFormat = currentCameraSettings.imageFormat
243                         )
244                     }
245                     ConcurrentCameraMode.DUAL -> {
246                         val primaryFacing = currentCameraSettings.cameraLensFacing
247                         val secondaryFacing = primaryFacing.flip()
248                         cameraProvider.availableConcurrentCameraInfos.firstNotNullOf {
249                             var primaryCameraInfo: CameraInfo? = null
250                             var secondaryCameraInfo: CameraInfo? = null
251                             it.forEach { cameraInfo ->
252                                 if (cameraInfo.appLensFacing == primaryFacing) {
253                                     primaryCameraInfo = cameraInfo
254                                 } else if (cameraInfo.appLensFacing == secondaryFacing) {
255                                     secondaryCameraInfo = cameraInfo
256                                 }
257                             }
258 
259                             primaryCameraInfo?.let { nonNullPrimary ->
260                                 secondaryCameraInfo?.let { nonNullSecondary ->
261                                     PerpetualSessionSettings.ConcurrentCamera(
262                                         primaryCameraInfo = nonNullPrimary,
263                                         secondaryCameraInfo = nonNullSecondary,
264                                         aspectRatio = currentCameraSettings.aspectRatio
265                                     )
266                                 }
267                             }
268                         }
269                     }
270                 }
271             }.distinctUntilChanged()
272             .collectLatest { sessionSettings ->
273                 coroutineScope {
274                     with(
275                         CameraSessionContext(
276                             context = application,
277                             cameraProvider = cameraProvider,
278                             backgroundDispatcher = defaultDispatcher,
279                             screenFlashEvents = screenFlashEvents,
280                             focusMeteringEvents = focusMeteringEvents,
281                             videoCaptureControlEvents = videoCaptureControlEvents,
282                             currentCameraState = _currentCameraState,
283                             surfaceRequests = _surfaceRequest,
284                             transientSettings = transientSettings
285                         )
286                     ) {
287                         try {
288                             when (sessionSettings) {
289                                 is PerpetualSessionSettings.SingleCamera -> runSingleCameraSession(
290                                     sessionSettings,
291                                     useCaseMode = useCaseMode
292                                 ) { imageCapture ->
293                                     imageCaptureUseCase = imageCapture
294                                 }
295 
296                                 is PerpetualSessionSettings.ConcurrentCamera ->
297                                     runConcurrentCameraSession(
298                                         sessionSettings,
299                                         useCaseMode = CameraUseCase.UseCaseMode.VIDEO_ONLY
300                                     )
301                             }
302                         } finally {
303                             // TODO(tm): This shouldn't be necessary. Cancellation of the
304                             //  coroutineScope by collectLatest should cause this to
305                             //  occur naturally.
306                             cameraProvider.unbindAll()
307                         }
308                     }
309                 }
310             }
311     }
312 
313     override suspend fun takePicture(onCaptureStarted: (() -> Unit)) {
314         if (imageCaptureUseCase == null) {
315             throw RuntimeException("Attempted take picture with null imageCapture use case")
316         }
317         try {
318             val imageProxy = imageCaptureUseCase!!.takePicture(onCaptureStarted)
319             Log.d(TAG, "onCaptureSuccess")
320             imageProxy.close()
321         } catch (exception: Exception) {
322             Log.d(TAG, "takePicture onError: $exception")
323             throw exception
324         }
325     }
326 
327     // TODO(b/319733374): Return bitmap for external mediastore capture without URI
328     override suspend fun takePicture(
329         onCaptureStarted: (() -> Unit),
330         contentResolver: ContentResolver,
331         imageCaptureUri: Uri?,
332         ignoreUri: Boolean
333     ): ImageCapture.OutputFileResults {
334         if (imageCaptureUseCase == null) {
335             throw RuntimeException("Attempted take picture with null imageCapture use case")
336         }
337         val eligibleContentValues = getEligibleContentValues()
338         val outputFileOptions: OutputFileOptions
339         if (ignoreUri) {
340             val formatter = SimpleDateFormat(
341                 "yyyy-MM-dd-HH-mm-ss-SSS",
342                 Locale.US
343             )
344             val filename = "JCA-${formatter.format(Calendar.getInstance().time)}.jpg"
345             val contentValues = ContentValues()
346             contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
347             contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
348             outputFileOptions = OutputFileOptions.Builder(
349                 contentResolver,
350                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
351                 contentValues
352             ).build()
353         } else if (imageCaptureUri == null) {
354             val e = RuntimeException("Null Uri is provided.")
355             Log.d(TAG, "takePicture onError: $e")
356             throw e
357         } else {
358             try {
359                 val outputStream = contentResolver.openOutputStream(imageCaptureUri)
360                 if (outputStream != null) {
361                     outputFileOptions =
362                         OutputFileOptions.Builder(
363                             contentResolver.openOutputStream(imageCaptureUri)!!
364                         ).build()
365                 } else {
366                     val e = RuntimeException("Provider recently crashed.")
367                     Log.d(TAG, "takePicture onError: $e")
368                     throw e
369                 }
370             } catch (e: FileNotFoundException) {
371                 Log.d(TAG, "takePicture onError: $e")
372                 throw e
373             }
374         }
375         try {
376             val outputFileResults = imageCaptureUseCase!!.takePicture(
377                 outputFileOptions,
378                 onCaptureStarted
379             )
380             val relativePath =
381                 eligibleContentValues.getAsString(MediaStore.Images.Media.RELATIVE_PATH)
382             val displayName = eligibleContentValues.getAsString(
383                 MediaStore.Images.Media.DISPLAY_NAME
384             )
385             Log.d(TAG, "Saved image to $relativePath/$displayName")
386             return outputFileResults
387         } catch (exception: ImageCaptureException) {
388             Log.d(TAG, "takePicture onError: $exception")
389             throw exception
390         }
391     }
392 
393     private fun getEligibleContentValues(): ContentValues {
394         val eligibleContentValues = ContentValues()
395         eligibleContentValues.put(
396             MediaStore.Images.Media.DISPLAY_NAME,
397             Calendar.getInstance().time.toString()
398         )
399         eligibleContentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
400         eligibleContentValues.put(
401             MediaStore.Images.Media.RELATIVE_PATH,
402             Environment.DIRECTORY_PICTURES
403         )
404         return eligibleContentValues
405     }
406 
407     override suspend fun startVideoRecording(
408         videoCaptureUri: Uri?,
409         shouldUseUri: Boolean,
410         onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit
411     ) {
412         if (shouldUseUri && videoCaptureUri == null) {
413             val e = RuntimeException("Null Uri is provided.")
414             Log.d(TAG, "takePicture onError: $e")
415             throw e
416         }
417         videoCaptureControlEvents.send(
418             VideoCaptureControlEvent.StartRecordingEvent(
419                 videoCaptureUri,
420                 shouldUseUri,
421                 onVideoRecord
422             )
423         )
424     }
425 
426     override fun stopVideoRecording() {
427         videoCaptureControlEvents.trySendBlocking(VideoCaptureControlEvent.StopRecordingEvent)
428     }
429 
430     override fun setZoomScale(scale: Float) {
431         currentSettings.update { old ->
432             old?.copy(zoomScale = scale)
433         }
434     }
435 
436     // Sets the camera to the designated lensFacing direction
437     override suspend fun setLensFacing(lensFacing: LensFacing) {
438         currentSettings.update { old ->
439             if (systemConstraints.availableLenses.contains(lensFacing)) {
440                 old?.copy(cameraLensFacing = lensFacing)
441                     ?.tryApplyDynamicRangeConstraints()
442                     ?.tryApplyImageFormatConstraints()
443             } else {
444                 old
445             }
446         }
447     }
448 
449     private fun CameraAppSettings.tryApplyDynamicRangeConstraints(): CameraAppSettings {
450         return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints ->
451             with(constraints.supportedDynamicRanges) {
452                 val newDynamicRange = if (contains(dynamicRange)) {
453                     dynamicRange
454                 } else {
455                     DynamicRange.SDR
456                 }
457 
458                 this@tryApplyDynamicRangeConstraints.copy(
459                     dynamicRange = newDynamicRange
460                 )
461             }
462         } ?: this
463     }
464 
465     private fun CameraAppSettings.tryApplyAspectRatioForExternalCapture(
466         useCaseMode: CameraUseCase.UseCaseMode
467     ): CameraAppSettings {
468         return when (useCaseMode) {
469             CameraUseCase.UseCaseMode.STANDARD -> this
470             CameraUseCase.UseCaseMode.IMAGE_ONLY ->
471                 this.copy(aspectRatio = AspectRatio.THREE_FOUR)
472 
473             CameraUseCase.UseCaseMode.VIDEO_ONLY ->
474                 this.copy(aspectRatio = AspectRatio.NINE_SIXTEEN)
475         }
476     }
477 
478     private fun CameraAppSettings.tryApplyImageFormatConstraints(): CameraAppSettings {
479         return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints ->
480             with(constraints.supportedImageFormatsMap[captureMode]) {
481                 val newImageFormat = if (this != null && contains(imageFormat)) {
482                     imageFormat
483                 } else {
484                     ImageOutputFormat.JPEG
485                 }
486 
487                 this@tryApplyImageFormatConstraints.copy(
488                     imageFormat = newImageFormat
489                 )
490             }
491         } ?: this
492     }
493 
494     private fun CameraAppSettings.tryApplyFrameRateConstraints(): CameraAppSettings {
495         return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints ->
496             with(constraints.supportedFixedFrameRates) {
497                 val newTargetFrameRate = if (contains(targetFrameRate)) {
498                     targetFrameRate
499                 } else {
500                     TARGET_FPS_AUTO
501                 }
502 
503                 this@tryApplyFrameRateConstraints.copy(
504                     targetFrameRate = newTargetFrameRate
505                 )
506             }
507         } ?: this
508     }
509 
510     private fun CameraAppSettings.tryApplyStabilizationConstraints(): CameraAppSettings {
511         return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints ->
512             with(constraints.supportedStabilizationModes) {
513                 val newVideoStabilization = if (contains(SupportedStabilizationMode.HIGH_QUALITY) &&
514                     (targetFrameRate != TARGET_FPS_60)
515                 ) {
516                     // unlike shouldVideoBeStabilized, doesn't check value of previewStabilization
517                     videoCaptureStabilization
518                 } else {
519                     Stabilization.UNDEFINED
520                 }
521                 val newPreviewStabilization = if (contains(SupportedStabilizationMode.ON) &&
522                     (targetFrameRate in setOf(TARGET_FPS_AUTO, TARGET_FPS_30))
523                 ) {
524                     previewStabilization
525                 } else {
526                     Stabilization.UNDEFINED
527                 }
528 
529                 this@tryApplyStabilizationConstraints.copy(
530                     previewStabilization = newPreviewStabilization,
531                     videoCaptureStabilization = newVideoStabilization
532                 )
533             }
534         } ?: this
535     }
536 
537     private fun CameraAppSettings.tryApplyConcurrentCameraModeConstraints(): CameraAppSettings =
538         when (concurrentCameraMode) {
539             ConcurrentCameraMode.OFF -> this
540             else ->
541                 if (systemConstraints.concurrentCamerasSupported) {
542                     copy(
543                         targetFrameRate = TARGET_FPS_AUTO,
544                         previewStabilization = Stabilization.OFF,
545                         videoCaptureStabilization = Stabilization.OFF,
546                         dynamicRange = DynamicRange.SDR,
547                         captureMode = CaptureMode.MULTI_STREAM
548                     )
549                 } else {
550                     copy(concurrentCameraMode = ConcurrentCameraMode.OFF)
551                 }
552         }
553 
554     override suspend fun tapToFocus(x: Float, y: Float) {
555         focusMeteringEvents.send(CameraEvent.FocusMeteringEvent(x, y))
556     }
557 
558     override fun getScreenFlashEvents() = screenFlashEvents
559     override fun getCurrentSettings() = currentSettings.asStateFlow()
560 
561     override fun setFlashMode(flashMode: FlashMode) {
562         currentSettings.update { old ->
563             old?.copy(flashMode = flashMode)
564         }
565     }
566 
567     override fun isScreenFlashEnabled() =
568         imageCaptureUseCase?.flashMode == ImageCapture.FLASH_MODE_SCREEN &&
569             imageCaptureUseCase?.screenFlash != null
570 
571     override suspend fun setAspectRatio(aspectRatio: AspectRatio) {
572         currentSettings.update { old ->
573             old?.copy(aspectRatio = aspectRatio)
574         }
575     }
576 
577     override suspend fun setCaptureMode(captureMode: CaptureMode) {
578         currentSettings.update { old ->
579             old?.copy(captureMode = captureMode)
580                 ?.tryApplyImageFormatConstraints()
581                 ?.tryApplyConcurrentCameraModeConstraints()
582         }
583     }
584 
585     override suspend fun setDynamicRange(dynamicRange: DynamicRange) {
586         currentSettings.update { old ->
587             old?.copy(dynamicRange = dynamicRange)
588                 ?.tryApplyConcurrentCameraModeConstraints()
589         }
590     }
591 
592     override fun setDeviceRotation(deviceRotation: DeviceRotation) {
593         currentSettings.update { old ->
594             old?.copy(deviceRotation = deviceRotation)
595         }
596     }
597 
598     override suspend fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) {
599         currentSettings.update { old ->
600             old?.copy(concurrentCameraMode = concurrentCameraMode)
601                 ?.tryApplyConcurrentCameraModeConstraints()
602         }
603     }
604 
605     override suspend fun setImageFormat(imageFormat: ImageOutputFormat) {
606         currentSettings.update { old ->
607             old?.copy(imageFormat = imageFormat)
608         }
609     }
610 
611     override suspend fun setPreviewStabilization(previewStabilization: Stabilization) {
612         currentSettings.update { old ->
613             old?.copy(
614                 previewStabilization = previewStabilization
615             )?.tryApplyStabilizationConstraints()
616                 ?.tryApplyConcurrentCameraModeConstraints()
617         }
618     }
619 
620     override suspend fun setVideoCaptureStabilization(videoCaptureStabilization: Stabilization) {
621         currentSettings.update { old ->
622             old?.copy(
623                 videoCaptureStabilization = videoCaptureStabilization
624             )?.tryApplyStabilizationConstraints()
625                 ?.tryApplyConcurrentCameraModeConstraints()
626         }
627     }
628 
629     override suspend fun setTargetFrameRate(targetFrameRate: Int) {
630         currentSettings.update { old ->
631             old?.copy(targetFrameRate = targetFrameRate)?.tryApplyFrameRateConstraints()
632                 ?.tryApplyConcurrentCameraModeConstraints()
633         }
634     }
635 
636     override suspend fun setLowLightBoost(lowLightBoost: LowLightBoost) {
637         currentSettings.update { old ->
638             old?.copy(lowLightBoost = lowLightBoost)
639         }
640     }
641 
642     override suspend fun setAudioMuted(isAudioMuted: Boolean) {
643         currentSettings.update { old ->
644             old?.copy(audioMuted = isAudioMuted)
645         }
646     }
647 
648     companion object {
649         private val FIXED_FRAME_RATES = setOf(TARGET_FPS_15, TARGET_FPS_30, TARGET_FPS_60)
650     }
651 }
652