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