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