From d868ff94c13763bb4215a2074125060e623e79e0 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 27 Nov 2024 23:00:39 +0000 Subject: [PATCH 01/13] update zoom through state changes --- .../core/camera/CameraSession.kt | 67 +++++++++++++++---- .../core/camera/CameraSessionContext.kt | 2 + .../core/camera/CameraSessionSettings.kt | 4 +- .../core/camera/CameraUseCase.kt | 15 ++--- .../core/camera/CameraXCameraUseCase.kt | 60 +++++++++-------- .../core/camera/test/FakeCameraUseCase.kt | 13 ++-- .../settings/model/CameraAppSettings.kt | 8 +-- .../settings/model/CameraZoomState.kt | 38 +++++++++++ .../feature/preview/PreviewScreen.kt | 5 +- .../feature/preview/PreviewUiState.kt | 8 +-- .../feature/preview/PreviewViewModel.kt | 7 +- .../preview/ui/CameraControlsOverlay.kt | 4 +- .../preview/ui/PreviewScreenComponents.kt | 7 +- .../ui/debug/DebugOverlayComponents.kt | 16 ++--- 14 files changed, 165 insertions(+), 89 deletions(-) create mode 100644 data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 5059dd56e..7c40939cd 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -62,6 +62,7 @@ import androidx.core.content.ContextCompat.checkSelfPermission import androidx.lifecycle.asFlow import com.google.jetpackcamera.core.camera.effects.SingleSurfaceForcingEffect import com.google.jetpackcamera.settings.model.AspectRatio +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.DeviceRotation import com.google.jetpackcamera.settings.model.DynamicRange @@ -148,6 +149,58 @@ internal suspend fun runSingleCameraSession( } } } +// update camerastate to mirror current zoomstate + launch { + camera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> + currentCameraState.update { old -> + old.copy( + zoomRatio = zoomState.zoomRatio, + linearZoomScale = zoomState.linearZoom + ) + } + } + } + + launch { + // Apply camera zoom + zoomChanges.filterNotNull().collectLatest { zoomChange -> + camera.cameraInfo.zoomState.value?.let { currentZoomState -> + when (zoomChange) { + is CameraZoomState.Ratio -> { + camera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + ) + + currentCameraState.update { old -> + old.copy(zoomRatio = zoomChange.value) + } + } + + is CameraZoomState.Linear -> { + camera.cameraControl.setLinearZoom(zoomChange.value) + currentCameraState.update { old -> + old.copy(zoomRatio = zoomChange.value) + } + } + + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + camera.cameraControl.setZoomRatio(newRatio) + currentCameraState.update { old -> + old.copy(zoomRatio = newRatio) + } + } + } + } + } + } applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup) processTransientSettingEvents( @@ -168,20 +221,6 @@ internal suspend fun processTransientSettingEvents( ) { var prevTransientSettings = initialTransientSettings transientSettings.filterNotNull().collectLatest { newTransientSettings -> - // Apply camera control settings - if (prevTransientSettings.zoomScale != newTransientSettings.zoomScale) { - camera.cameraInfo.zoomState.value?.let { zoomState -> - val finalScale = - (zoomState.zoomRatio * newTransientSettings.zoomScale).coerceIn( - zoomState.minZoomRatio, - zoomState.maxZoomRatio - ) - camera.cameraControl.setZoomRatio(finalScale) - currentCameraState.update { old -> - old.copy(zoomScale = finalScale) - } - } - } useCaseGroup.getImageCapture()?.let { imageCapture -> if (prevTransientSettings.flashMode != newTransientSettings.flashMode) { diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt index 1425bbbb8..a2e219136 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt @@ -18,6 +18,7 @@ package com.google.jetpackcamera.core.camera import android.content.Context import androidx.camera.core.SurfaceRequest import androidx.camera.lifecycle.ProcessCameraProvider +import com.google.jetpackcamera.settings.model.CameraZoomState import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.SendChannel @@ -37,6 +38,7 @@ internal data class CameraSessionContext( val screenFlashEvents: SendChannel, val focusMeteringEvents: Channel, val videoCaptureControlEvents: Channel, + val zoomChanges: StateFlow, val currentCameraState: MutableStateFlow, val surfaceRequests: MutableStateFlow, val transientSettings: StateFlow diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt index 37298f321..244735c62 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt @@ -61,5 +61,7 @@ internal data class TransientSessionSettings( val isAudioMuted: Boolean, val deviceRotation: DeviceRotation, val flashMode: FlashMode, - val zoomScale: Float + val cameraInfo: CameraInfo, + val frontZoomRatio: Float, + val rearZoomRatio: Float ) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt index c58778f08..862847936 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt @@ -21,6 +21,7 @@ import androidx.camera.core.ImageCapture import androidx.camera.core.SurfaceRequest import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation @@ -83,7 +84,7 @@ interface CameraUseCase { suspend fun stopVideoRecording() - fun setZoomScale(scale: Float) + fun changeZoom(newZoomState: CameraZoomState) fun getCurrentCameraState(): StateFlow @@ -153,9 +154,7 @@ sealed interface VideoRecordingState { /** * Camera is not currently recording a video */ - data class Inactive( - val finalElapsedTimeNanos: Long = 0 - ) : VideoRecordingState + data class Inactive(val finalElapsedTimeNanos: Long = 0) : VideoRecordingState /** * Camera is currently active; paused, stopping, or recording a video @@ -181,14 +180,12 @@ sealed interface VideoRecordingState { data class CameraState( val videoRecordingState: VideoRecordingState = VideoRecordingState.Inactive(), - val zoomScale: Float = 1f, + val zoomRatio: Float = 1f, + val linearZoomScale: Float? = null, val sessionFirstFrameTimestamp: Long = 0L, val torchEnabled: Boolean = false, val stabilizationMode: StabilizationMode = StabilizationMode.OFF, val debugInfo: DebugInfo = DebugInfo(null, null) ) -data class DebugInfo( - val logicalCameraId: String?, - val physicalCameraId: String? -) +data class DebugInfo(val logicalCameraId: String?, val physicalCameraId: String?) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index fa16ca43a..d693398c8 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -42,6 +42,7 @@ import com.google.jetpackcamera.settings.SettableConstraintsRepository import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraConstraints +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation @@ -112,6 +113,9 @@ constructor( private val currentSettings = MutableStateFlow(null) + // todo: zoomchanges init with cameraappsettings zoomratio + private val _zoomChanges = MutableStateFlow(null) + // Could be improved by setting initial value only when camera is initialized private var _currentCameraState = MutableStateFlow(CameraState()) override fun getCurrentCameraState(): StateFlow = _currentCameraState.asStateFlow() @@ -260,11 +264,17 @@ constructor( currentSettings .filterNotNull() .map { currentCameraSettings -> + val cameraSelector = when (currentCameraSettings.cameraLensFacing) { + LensFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA + LensFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA + } transientSettings.value = TransientSessionSettings( isAudioMuted = currentCameraSettings.audioMuted, deviceRotation = currentCameraSettings.deviceRotation, flashMode = currentCameraSettings.flashMode, - zoomScale = currentCameraSettings.zoomScale + cameraInfo = cameraProvider.getCameraInfo(cameraSelector), + frontZoomRatio = currentCameraSettings.frontZoomRatio, + rearZoomRatio = currentCameraSettings.rearZoomRatio ) val cameraConstraints = checkNotNull( @@ -335,6 +345,7 @@ constructor( screenFlashEvents = screenFlashEvents, focusMeteringEvents = focusMeteringEvents, videoCaptureControlEvents = videoCaptureControlEvents, + zoomChanges = _zoomChanges.asStateFlow(), currentCameraState = _currentCameraState, surfaceRequests = _surfaceRequest, transientSettings = transientSettings @@ -529,9 +540,9 @@ constructor( videoCaptureControlEvents.send(VideoCaptureControlEvent.StopRecordingEvent) } - override fun setZoomScale(scale: Float) { - currentSettings.update { old -> - old?.copy(zoomScale = scale) + override fun changeZoom(newZoomState: CameraZoomState) { + _zoomChanges.update { + newZoomState } } @@ -549,8 +560,8 @@ constructor( } } - private fun CameraAppSettings.tryApplyDynamicRangeConstraints(): CameraAppSettings { - return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + private fun CameraAppSettings.tryApplyDynamicRangeConstraints(): CameraAppSettings = + systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedDynamicRanges) { val newDynamicRange = if (contains(dynamicRange)) { dynamicRange @@ -563,23 +574,20 @@ constructor( ) } } ?: this - } private fun CameraAppSettings.tryApplyAspectRatioForExternalCapture( useCaseMode: CameraUseCase.UseCaseMode - ): CameraAppSettings { - return when (useCaseMode) { - CameraUseCase.UseCaseMode.STANDARD -> this - CameraUseCase.UseCaseMode.IMAGE_ONLY -> - this.copy(aspectRatio = AspectRatio.THREE_FOUR) - - CameraUseCase.UseCaseMode.VIDEO_ONLY -> - this.copy(aspectRatio = AspectRatio.NINE_SIXTEEN) - } + ): CameraAppSettings = when (useCaseMode) { + CameraUseCase.UseCaseMode.STANDARD -> this + CameraUseCase.UseCaseMode.IMAGE_ONLY -> + this.copy(aspectRatio = AspectRatio.THREE_FOUR) + + CameraUseCase.UseCaseMode.VIDEO_ONLY -> + this.copy(aspectRatio = AspectRatio.NINE_SIXTEEN) } - private fun CameraAppSettings.tryApplyImageFormatConstraints(): CameraAppSettings { - return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + private fun CameraAppSettings.tryApplyImageFormatConstraints(): CameraAppSettings = + systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedImageFormatsMap[captureMode]) { val newImageFormat = if (this != null && contains(imageFormat)) { imageFormat @@ -592,10 +600,9 @@ constructor( ) } } ?: this - } - private fun CameraAppSettings.tryApplyFrameRateConstraints(): CameraAppSettings { - return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + private fun CameraAppSettings.tryApplyFrameRateConstraints(): CameraAppSettings = + systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedFixedFrameRates) { val newTargetFrameRate = if (contains(targetFrameRate)) { targetFrameRate @@ -608,10 +615,9 @@ constructor( ) } } ?: this - } - private fun CameraAppSettings.tryApplyStabilizationConstraints(): CameraAppSettings { - return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + private fun CameraAppSettings.tryApplyStabilizationConstraints(): CameraAppSettings = + systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> val invalidFps = when (stabilizationMode) { StabilizationMode.ON -> STABILIZATION_ON_UNSUPPORTED_FPS StabilizationMode.HIGH_QUALITY -> STABILIZATION_HIGH_QUALITY_UNSUPPORTED_FPS @@ -634,7 +640,6 @@ constructor( stabilizationMode = newStabilizationMode ) } ?: this - } private fun CameraAppSettings.tryApplyConcurrentCameraModeConstraints(): CameraAppSettings = when (concurrentCameraMode) { @@ -651,8 +656,8 @@ constructor( } } - private fun CameraAppSettings.tryApplyFlashModeConstraints(): CameraAppSettings { - return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + private fun CameraAppSettings.tryApplyFlashModeConstraints(): CameraAppSettings = + systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedFlashModes) { val newFlashMode = if (contains(flashMode)) { flashMode @@ -665,7 +670,6 @@ constructor( ) } } ?: this - } override suspend fun tapToFocus(x: Float, y: Float) { focusMeteringEvents.send(CameraEvent.FocusMeteringEvent(x, y)) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt index 7baf6d6ca..e3c77d791 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraUseCase.kt @@ -24,6 +24,7 @@ import com.google.jetpackcamera.core.camera.CameraState import com.google.jetpackcamera.core.camera.CameraUseCase import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation @@ -58,7 +59,7 @@ class FakeCameraUseCase( private var isScreenFlash = true private var screenFlashEvents = Channel(capacity = UNLIMITED) - + private val zoomChanges = MutableStateFlow(null) private val currentSettings = MutableStateFlow(defaultCameraSettings) override suspend fun initialize( @@ -93,10 +94,6 @@ class FakeCameraUseCase( isScreenFlash = isLensFacingFront && (it.flashMode == FlashMode.AUTO || it.flashMode == FlashMode.ON) - - _currentCameraState.update { old -> - old.copy(zoomScale = it.zoomScale) - } } } @@ -154,9 +151,9 @@ class FakeCameraUseCase( } private val _currentCameraState = MutableStateFlow(CameraState()) - override fun setZoomScale(scale: Float) { - currentSettings.update { old -> - old.copy(zoomScale = scale) + override fun changeZoom(newZoomState: CameraZoomState) { + zoomChanges.update { old -> + newZoomState } } override fun getCurrentCameraState(): StateFlow = _currentCameraState.asStateFlow() diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt index ba7a03e77..b74a757b3 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt @@ -30,7 +30,8 @@ data class CameraAppSettings( val dynamicRange: DynamicRange = DynamicRange.SDR, val defaultHdrDynamicRange: DynamicRange = DynamicRange.HLG10, val defaultHdrImageOutputFormat: ImageOutputFormat = ImageOutputFormat.JPEG_ULTRA_HDR, - val zoomScale: Float = 1f, + val rearZoomRatio: Float = 1f, + val frontZoomRatio: Float = 1f, val targetFrameRate: Int = TARGET_FPS_AUTO, val imageFormat: ImageOutputFormat = ImageOutputFormat.JPEG, val audioMuted: Boolean = false, @@ -39,8 +40,7 @@ data class CameraAppSettings( val maxVideoDurationMillis: Long = UNLIMITED_VIDEO_DURATION ) -fun SystemConstraints.forCurrentLens(cameraAppSettings: CameraAppSettings): CameraConstraints? { - return perLensConstraints[cameraAppSettings.cameraLensFacing] -} +fun SystemConstraints.forCurrentLens(cameraAppSettings: CameraAppSettings): CameraConstraints? = + perLensConstraints[cameraAppSettings.cameraLensFacing] val DEFAULT_CAMERA_APP_SETTINGS = CameraAppSettings() diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt new file mode 100644 index 000000000..8c6259921 --- /dev/null +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.jetpackcamera.settings.model/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface CameraZoomState { + val value: Float + data class Scale(override val value: Float) : CameraZoomState + data class Linear(override val value: Float) : CameraZoomState + data class Ratio(override val value: Float) : CameraZoomState +} diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index d9cae762f..794e7bf49 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -62,6 +62,7 @@ import com.google.jetpackcamera.feature.preview.ui.ZoomLevelDisplayState import com.google.jetpackcamera.feature.preview.ui.debouncedOrientationFlow import com.google.jetpackcamera.feature.preview.ui.debug.DebugOverlayComponent import com.google.jetpackcamera.settings.model.AspectRatio +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS @@ -140,7 +141,7 @@ fun PreviewScreen( onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness, onSetLensFacing = viewModel::setLensFacing, onTapToFocus = viewModel::tapToFocus, - onChangeZoomScale = viewModel::setZoomScale, + onChangeZoomScale = viewModel::setZoom, onChangeFlash = viewModel::setFlash, onChangeAspectRatio = viewModel::setAspectRatio, onChangeCaptureMode = viewModel::setCaptureMode, @@ -175,7 +176,7 @@ private fun ContentScreen( onClearUiScreenBrightness: (Float) -> Unit = {}, onSetLensFacing: (newLensFacing: LensFacing) -> Unit = {}, onTapToFocus: (x: Float, y: Float) -> Unit = { _, _ -> }, - onChangeZoomScale: (Float) -> Unit = {}, + onChangeZoomScale: (CameraZoomState) -> Unit = {}, onChangeFlash: (FlashMode) -> Unit = {}, onChangeAspectRatio: (AspectRatio) -> Unit = {}, onChangeCaptureMode: (CaptureMode) -> Unit = {}, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt index 36f869c33..ec34d89c1 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt @@ -33,7 +33,7 @@ sealed interface PreviewUiState { // "quick" settings val currentCameraSettings: CameraAppSettings = CameraAppSettings(), val systemConstraints: SystemConstraints = SystemConstraints(), - val zoomScale: Float = 1f, + val primaryZoomRatio: Float = 1f, val videoRecordingState: VideoRecordingState = VideoRecordingState.Inactive(), val quickSettingsIsOpen: Boolean = false, val audioMuted: Boolean = false, @@ -64,10 +64,8 @@ data class DebugUiState( sealed interface StabilizationUiState { data object Disabled : StabilizationUiState - data class Set( - val stabilizationMode: StabilizationMode, - val active: Boolean = true - ) : StabilizationUiState + data class Set(val stabilizationMode: StabilizationMode, val active: Boolean = true) : + StabilizationUiState } sealed class FlashModeUiState { diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index 002a7a267..0f8b6ef6c 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -39,6 +39,7 @@ import com.google.jetpackcamera.settings.SettingsRepository import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CameraConstraints +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.ConcurrentCameraMode import com.google.jetpackcamera.settings.model.DeviceRotation @@ -174,7 +175,7 @@ class PreviewViewModel @AssistedInject constructor( previewMode = previewMode, currentCameraSettings = cameraAppSettings, systemConstraints = systemConstraints, - zoomScale = cameraState.zoomScale, + primaryZoomRatio = cameraState.zoomRatio, videoRecordingState = cameraState.videoRecordingState, sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp, captureModeToggleUiState = getCaptureToggleUiState( @@ -793,8 +794,8 @@ class PreviewViewModel @AssistedInject constructor( } } - fun setZoomScale(scale: Float) { - cameraUseCase.setZoomScale(scale = scale) + fun setZoom(newZoomState: CameraZoomState) { + cameraUseCase.changeZoom(newZoomState = newZoomState) } fun setDynamicRange(dynamicRange: DynamicRange) { diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt index 873cd1838..3ee1b4723 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt @@ -116,7 +116,7 @@ fun CameraControlsOverlay( ) { // Show the current zoom level for a short period of time, only when the level changes. var firstRun by remember { mutableStateOf(true) } - LaunchedEffect(previewUiState.zoomScale) { + LaunchedEffect(previewUiState.primaryZoomRatio) { if (firstRun) { firstRun = false } else { @@ -147,7 +147,7 @@ fun CameraControlsOverlay( .fillMaxWidth() .align(Alignment.BottomCenter), previewUiState = previewUiState, - zoomLevel = previewUiState.zoomScale, + zoomLevel = previewUiState.primaryZoomRatio, physicalCameraId = previewUiState.currentPhysicalCameraId, logicalCameraId = previewUiState.currentLogicalCameraId, showZoomLevel = zoomLevelDisplayState.showZoomLevel, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt index a92527446..b1eebd2f4 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt @@ -108,6 +108,7 @@ import com.google.jetpackcamera.feature.preview.PreviewUiState import com.google.jetpackcamera.feature.preview.R import com.google.jetpackcamera.feature.preview.ui.theme.PreviewPreviewTheme import com.google.jetpackcamera.settings.model.AspectRatio +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.StabilizationMode import kotlin.time.Duration.Companion.nanoseconds import kotlinx.coroutines.delay @@ -373,15 +374,15 @@ fun PreviewDisplay( previewUiState: PreviewUiState.Ready, onTapToFocus: (x: Float, y: Float) -> Unit, onFlipCamera: () -> Unit, - onZoomChange: (Float) -> Unit, + onZoomChange: (CameraZoomState) -> Unit, onRequestWindowColorMode: (Int) -> Unit, aspectRatio: AspectRatio, surfaceRequest: SurfaceRequest?, modifier: Modifier = Modifier ) { val transformableState = rememberTransformableState( - onTransformation = { zoomChange, _, _ -> - onZoomChange(zoomChange) + onTransformation = { pinchZoomChange, _, _ -> + onZoomChange(CameraZoomState.Scale(pinchZoomChange)) } ) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt index 14a31f5f0..ac3a3bbfc 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/debug/DebugOverlayComponents.kt @@ -33,10 +33,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -51,6 +49,7 @@ import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_SET_ZOOM_RATIO_ import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_SET_ZOOM_RATIO_SET_BUTTON import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_SET_ZOOM_RATIO_TEXT_FIELD import com.google.jetpackcamera.feature.preview.ui.DEBUG_OVERLAY_SHOW_CAMERA_PROPERTIES_BUTTON +import com.google.jetpackcamera.settings.model.CameraZoomState private const val TAG = "DebugOverlayComponents" @@ -64,7 +63,7 @@ fun DebugOverlayToggleButton(modifier: Modifier = Modifier, toggleIsOpen: () -> @Composable fun DebugOverlayComponent( modifier: Modifier = Modifier, - onChangeZoomScale: (Float) -> Unit, + onChangeZoomScale: (CameraZoomState) -> Unit, toggleIsOpen: () -> Unit, previewUiState: PreviewUiState.Ready ) { @@ -134,7 +133,7 @@ fun DebugOverlayComponent( // Set zoom ratio if (zoomRatioDialog.value) { - SetZoomRatioComponent(previewUiState, onChangeZoomScale) { + SetZoomRatioComponent(onChangeZoomScale) { zoomRatioDialog.value = false } } @@ -165,8 +164,7 @@ private fun CameraPropertiesJSONComponent( @Composable private fun SetZoomRatioComponent( - previewUiState: PreviewUiState.Ready, - onChangeZoomScale: (Float) -> Unit, + onChangeZoomRatio: (CameraZoomState.Ratio) -> Unit, onClose: () -> Unit ) { var zoomRatioText = remember { mutableStateOf("") } @@ -191,14 +189,12 @@ private fun SetZoomRatioComponent( ), onClick = { try { - val relativeRatio = if (zoomRatioText.value.isEmpty()) { + val newRatio = if (zoomRatioText.value.isEmpty()) { 1f } else { zoomRatioText.value.toFloat() } - val currentRatio = previewUiState.zoomScale - val absoluteRatio = relativeRatio / currentRatio - onChangeZoomScale(absoluteRatio) + onChangeZoomRatio(CameraZoomState.Ratio(newRatio)) } catch (e: NumberFormatException) { Log.d(TAG, "Zoom ratio should be a float") } From 4c754f2088b44286d665265d1ed478cad96b4198 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Mon, 9 Dec 2024 20:32:21 +0000 Subject: [PATCH 02/13] zoom concurrent primary camera --- .../core/camera/ConcurrentCameraSession.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index b936a1fc6..0ccb44d1a 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -20,6 +20,7 @@ import android.util.Log import androidx.camera.core.CompositionSettings import androidx.camera.core.TorchState import androidx.lifecycle.asFlow +import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.ImageOutputFormat import com.google.jetpackcamera.settings.model.StabilizationMode @@ -106,6 +107,59 @@ internal suspend fun runConcurrentCameraSession( } } + // update camerastate to mirror current zoomstate + launch { + primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> + currentCameraState.update { old -> + old.copy( + zoomRatio = zoomState.zoomRatio, + linearZoomScale = zoomState.linearZoom + ) + } + } + } + + launch { + // Apply camera zoom + zoomChanges.filterNotNull().collectLatest { zoomChange -> + primaryCamera.cameraInfo.zoomState.value?.let { currentZoomState -> + when (zoomChange) { + is CameraZoomState.Ratio -> { + primaryCamera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + ) + + currentCameraState.update { old -> + old.copy(zoomRatio = zoomChange.value) + } + } + + is CameraZoomState.Linear -> { + primaryCamera.cameraControl.setLinearZoom(zoomChange.value) + currentCameraState.update { old -> + old.copy(zoomRatio = zoomChange.value) + } + } + + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + primaryCamera.cameraControl.setZoomRatio(newRatio) + currentCameraState.update { old -> + old.copy(zoomRatio = newRatio) + } + } + } + } + } + } + applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup) processTransientSettingEvents( primaryCamera, From 0c30c653ddb5639838e8f85e0dabe2152d84dd5b Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 10 Dec 2024 01:17:03 +0000 Subject: [PATCH 03/13] support zoom concurrent --- .../core/camera/CameraSession.kt | 79 ++++++++----------- .../core/camera/ConcurrentCameraSession.kt | 11 +-- 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 7c40939cd..f1f9081cf 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -150,57 +150,47 @@ internal suspend fun runSingleCameraSession( } } // update camerastate to mirror current zoomstate - launch { - camera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> - currentCameraState.update { old -> - old.copy( - zoomRatio = zoomState.zoomRatio, - linearZoomScale = zoomState.linearZoom - ) - } + launch { + camera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> + currentCameraState.update { old -> + old.copy( + zoomRatio = zoomState.zoomRatio, + linearZoomScale = zoomState.linearZoom + ) } } + } - launch { - // Apply camera zoom - zoomChanges.filterNotNull().collectLatest { zoomChange -> - camera.cameraInfo.zoomState.value?.let { currentZoomState -> - when (zoomChange) { - is CameraZoomState.Ratio -> { - camera.cameraControl.setZoomRatio( - zoomChange.value.coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio - ) + launch { + // Apply camera zoom + zoomChanges.filterNotNull().collectLatest { zoomChange -> + camera.cameraInfo.zoomState.value?.let { currentZoomState -> + when (zoomChange) { + is CameraZoomState.Ratio -> { + camera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio ) + ) + } + + is CameraZoomState.Linear -> { + camera.cameraControl.setLinearZoom(zoomChange.value) + } - currentCameraState.update { old -> - old.copy(zoomRatio = zoomChange.value) - } - } - - is CameraZoomState.Linear -> { - camera.cameraControl.setLinearZoom(zoomChange.value) - currentCameraState.update { old -> - old.copy(zoomRatio = zoomChange.value) - } - } - - is CameraZoomState.Scale -> { - val newRatio = - (currentZoomState.zoomRatio * zoomChange.value).coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio - ) - camera.cameraControl.setZoomRatio(newRatio) - currentCameraState.update { old -> - old.copy(zoomRatio = newRatio) - } - } + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + camera.cameraControl.setZoomRatio(newRatio) } } } } + } applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup) processTransientSettingEvents( @@ -393,8 +383,8 @@ private fun createVideoUseCase( }.build() } -private fun getAspectRatioForUseCase(sensorLandscapeRatio: Float, aspectRatio: AspectRatio): Int { - return when (aspectRatio) { +private fun getAspectRatioForUseCase(sensorLandscapeRatio: Float, aspectRatio: AspectRatio): Int = + when (aspectRatio) { AspectRatio.THREE_FOUR -> androidx.camera.core.AspectRatio.RATIO_4_3 AspectRatio.NINE_SIXTEEN -> androidx.camera.core.AspectRatio.RATIO_16_9 else -> { @@ -409,7 +399,6 @@ private fun getAspectRatioForUseCase(sensorLandscapeRatio: Float, aspectRatio: A } } } -} context(CameraSessionContext) private fun createPreviewUseCase( diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index 0ccb44d1a..c4fe5d900 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -83,6 +83,7 @@ internal suspend fun runConcurrentCameraSession( cameraProvider.runWithConcurrent(cameraConfigs, useCaseGroup) { concurrentCamera -> Log.d(TAG, "Concurrent camera session started") + // a bug? concurrent camera only ever lists one camera val primaryCamera = concurrentCamera.cameras.first { it.cameraInfo.appLensFacing == sessionSettings.primaryCameraInfo.appLensFacing } @@ -131,17 +132,10 @@ internal suspend fun runConcurrentCameraSession( currentZoomState.maxZoomRatio ) ) - - currentCameraState.update { old -> - old.copy(zoomRatio = zoomChange.value) - } } is CameraZoomState.Linear -> { primaryCamera.cameraControl.setLinearZoom(zoomChange.value) - currentCameraState.update { old -> - old.copy(zoomRatio = zoomChange.value) - } } is CameraZoomState.Scale -> { @@ -151,9 +145,6 @@ internal suspend fun runConcurrentCameraSession( currentZoomState.maxZoomRatio ) primaryCamera.cameraControl.setZoomRatio(newRatio) - currentCameraState.update { old -> - old.copy(zoomRatio = newRatio) - } } } } From 90cf062dc36547007cb654b64eb4c38c2935ea33 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 10 Dec 2024 21:17:54 +0000 Subject: [PATCH 04/13] spotless --- .../core/camera/CameraXCameraUseCase.kt | 12 ++++++------ .../settings/model/CameraZoomState.kt | 17 +---------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index d693398c8..2a3b55606 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -114,11 +114,11 @@ constructor( private val currentSettings = MutableStateFlow(null) // todo: zoomchanges init with cameraappsettings zoomratio - private val _zoomChanges = MutableStateFlow(null) + private val zoomChanges = MutableStateFlow(null) // Could be improved by setting initial value only when camera is initialized - private var _currentCameraState = MutableStateFlow(CameraState()) - override fun getCurrentCameraState(): StateFlow = _currentCameraState.asStateFlow() + private var currentCameraState = MutableStateFlow(CameraState()) + override fun getCurrentCameraState(): StateFlow = currentCameraState.asStateFlow() private val _surfaceRequest = MutableStateFlow(null) @@ -345,8 +345,8 @@ constructor( screenFlashEvents = screenFlashEvents, focusMeteringEvents = focusMeteringEvents, videoCaptureControlEvents = videoCaptureControlEvents, - zoomChanges = _zoomChanges.asStateFlow(), - currentCameraState = _currentCameraState, + zoomChanges = zoomChanges.asStateFlow(), + currentCameraState = currentCameraState, surfaceRequests = _surfaceRequest, transientSettings = transientSettings ) @@ -541,7 +541,7 @@ constructor( } override fun changeZoom(newZoomState: CameraZoomState) { - _zoomChanges.update { + zoomChanges.update { newZoomState } } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt index 8c6259921..e26a8dc02 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraZoomState.kt @@ -13,22 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package com.google.jetpackcamera.settings.model/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +package com.google.jetpackcamera.settings.model interface CameraZoomState { val value: Float From 632044d58e42fea4254e038ae381a151f5a9a409 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 11 Dec 2024 16:46:55 +0000 Subject: [PATCH 05/13] store zoom using maps --- .../core/camera/CameraSession.kt | 8 +++++-- .../core/camera/CameraSessionSettings.kt | 3 +-- .../core/camera/CameraUseCase.kt | 4 ++-- .../core/camera/CameraXCameraUseCase.kt | 3 +-- .../core/camera/ConcurrentCameraSession.kt | 8 +++++-- .../settings/model/CameraAppSettings.kt | 3 +-- .../feature/preview/PreviewUiState.kt | 3 ++- .../feature/preview/PreviewViewModel.kt | 2 +- .../preview/ui/CameraControlsOverlay.kt | 22 +++++++++++-------- .../preview/ui/PreviewScreenComponents.kt | 4 ++-- 10 files changed, 35 insertions(+), 25 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index f1f9081cf..28160348c 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -154,8 +154,12 @@ internal suspend fun runSingleCameraSession( camera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> currentCameraState.update { old -> old.copy( - zoomRatio = zoomState.zoomRatio, - linearZoomScale = zoomState.linearZoom + zoomRatios = old.zoomRatios.toMutableMap().apply { + put(camera.cameraInfo.appLensFacing, zoomState.zoomRatio) + }, + linearZoomScales = old.linearZoomScales.toMutableMap().apply { + put(camera.cameraInfo.appLensFacing, zoomState.linearZoom) + } ) } } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt index 244735c62..90d6db240 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt @@ -62,6 +62,5 @@ internal data class TransientSessionSettings( val deviceRotation: DeviceRotation, val flashMode: FlashMode, val cameraInfo: CameraInfo, - val frontZoomRatio: Float, - val rearZoomRatio: Float + val zoomRatios: Map ) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt index 862847936..bdcd8299c 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraUseCase.kt @@ -180,8 +180,8 @@ sealed interface VideoRecordingState { data class CameraState( val videoRecordingState: VideoRecordingState = VideoRecordingState.Inactive(), - val zoomRatio: Float = 1f, - val linearZoomScale: Float? = null, + val zoomRatios: Map = mapOf(), + val linearZoomScales: Map = mapOf(), val sessionFirstFrameTimestamp: Long = 0L, val torchEnabled: Boolean = false, val stabilizationMode: StabilizationMode = StabilizationMode.OFF, diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index 2a3b55606..6bfd0f752 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -273,8 +273,7 @@ constructor( deviceRotation = currentCameraSettings.deviceRotation, flashMode = currentCameraSettings.flashMode, cameraInfo = cameraProvider.getCameraInfo(cameraSelector), - frontZoomRatio = currentCameraSettings.frontZoomRatio, - rearZoomRatio = currentCameraSettings.rearZoomRatio + zoomRatios = currentCameraSettings.defaultZoomRatios ) val cameraConstraints = checkNotNull( diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index c4fe5d900..ba565f135 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -113,8 +113,12 @@ internal suspend fun runConcurrentCameraSession( primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> currentCameraState.update { old -> old.copy( - zoomRatio = zoomState.zoomRatio, - linearZoomScale = zoomState.linearZoom + zoomRatios = old.zoomRatios.toMutableMap().apply { + put(primaryCamera.cameraInfo.appLensFacing, zoomState.zoomRatio) + }.toMap(), + linearZoomScales = old.linearZoomScales.toMutableMap().apply { + put(primaryCamera.cameraInfo.appLensFacing, zoomState.linearZoom) + }.toMap() ) } } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt index b74a757b3..df56a854f 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt @@ -30,8 +30,7 @@ data class CameraAppSettings( val dynamicRange: DynamicRange = DynamicRange.SDR, val defaultHdrDynamicRange: DynamicRange = DynamicRange.HLG10, val defaultHdrImageOutputFormat: ImageOutputFormat = ImageOutputFormat.JPEG_ULTRA_HDR, - val rearZoomRatio: Float = 1f, - val frontZoomRatio: Float = 1f, + val defaultZoomRatios: Map = mapOf(), val targetFrameRate: Int = TARGET_FPS_AUTO, val imageFormat: ImageOutputFormat = ImageOutputFormat.JPEG, val audioMuted: Boolean = false, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt index ec34d89c1..c5893bd94 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewUiState.kt @@ -20,6 +20,7 @@ import com.google.jetpackcamera.feature.preview.ui.SnackbarData import com.google.jetpackcamera.feature.preview.ui.ToastMessage import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.FlashMode +import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.StabilizationMode import com.google.jetpackcamera.settings.model.SystemConstraints @@ -33,7 +34,7 @@ sealed interface PreviewUiState { // "quick" settings val currentCameraSettings: CameraAppSettings = CameraAppSettings(), val systemConstraints: SystemConstraints = SystemConstraints(), - val primaryZoomRatio: Float = 1f, + val zoomRatios: Map = mapOf(), val videoRecordingState: VideoRecordingState = VideoRecordingState.Inactive(), val quickSettingsIsOpen: Boolean = false, val audioMuted: Boolean = false, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index 0f8b6ef6c..7f5254f7c 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -175,7 +175,7 @@ class PreviewViewModel @AssistedInject constructor( previewMode = previewMode, currentCameraSettings = cameraAppSettings, systemConstraints = systemConstraints, - primaryZoomRatio = cameraState.zoomRatio, + zoomRatios = cameraState.zoomRatios, videoRecordingState = cameraState.videoRecordingState, sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp, captureModeToggleUiState = getCaptureToggleUiState( diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt index 3ee1b4723..9d9589d15 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/CameraControlsOverlay.kt @@ -116,7 +116,7 @@ fun CameraControlsOverlay( ) { // Show the current zoom level for a short period of time, only when the level changes. var firstRun by remember { mutableStateOf(true) } - LaunchedEffect(previewUiState.primaryZoomRatio) { + LaunchedEffect(previewUiState.zoomRatios) { if (firstRun) { firstRun = false } else { @@ -147,7 +147,7 @@ fun CameraControlsOverlay( .fillMaxWidth() .align(Alignment.BottomCenter), previewUiState = previewUiState, - zoomLevel = previewUiState.primaryZoomRatio, + zoomRatios = previewUiState.zoomRatios, physicalCameraId = previewUiState.currentPhysicalCameraId, logicalCameraId = previewUiState.currentLogicalCameraId, showZoomLevel = zoomLevelDisplayState.showZoomLevel, @@ -242,7 +242,7 @@ private fun ControlsBottom( previewUiState: PreviewUiState.Ready, physicalCameraId: String? = null, logicalCameraId: String? = null, - zoomLevel: Float, + zoomRatios: Map = mapOf(), showZoomLevel: Boolean, isQuickSettingsOpen: Boolean, systemConstraints: SystemConstraints, @@ -272,7 +272,11 @@ private fun ControlsBottom( ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { if (showZoomLevel) { - ZoomScaleText(zoomLevel) + zoomRatios[previewUiState.currentCameraSettings.cameraLensFacing]?.let { + zoomRatio + -> + ZoomRatioText(zoomRatio) + } } if (previewUiState.debugUiState.isDebugMode) { CurrentCameraIdText(physicalCameraId, logicalCameraId) @@ -573,7 +577,7 @@ private fun Preview_ControlsBottom() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, @@ -594,7 +598,7 @@ private fun Preview_ControlsBottom_NoZoomLevel() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = false, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, @@ -615,7 +619,7 @@ private fun Preview_ControlsBottom_QuickSettingsOpen() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = true, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, @@ -636,7 +640,7 @@ private fun Preview_ControlsBottom_NoFlippableCamera() { captureModeToggleUiState = CaptureModeToggleUiState.Invisible, videoRecordingState = VideoRecordingState.Inactive() ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS.copy( @@ -664,7 +668,7 @@ private fun Preview_ControlsBottom_Recording() { videoRecordingState = VideoRecordingState.Active.Recording(0L, .9, 1_000_000_000) ), - zoomLevel = 1.3f, + zoomRatios = mapOf(Pair(LensFacing.BACK, 1.3f), Pair(LensFacing.FRONT, 1.0f)), showZoomLevel = true, isQuickSettingsOpen = false, systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt index b1eebd2f4..d0ffb7e2e 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/ui/PreviewScreenComponents.kt @@ -559,7 +559,7 @@ fun SettingsNavButton(onNavigateToSettings: () -> Unit, modifier: Modifier = Mod } @Composable -fun ZoomScaleText(zoomScale: Float) { +fun ZoomRatioText(zoomRatio: Float) { val contentAlpha = animateFloatAsState( targetValue = 10f, label = "zoomScaleAlphaAnimation", @@ -569,7 +569,7 @@ fun ZoomScaleText(zoomScale: Float) { modifier = Modifier .alpha(contentAlpha.value) .testTag(ZOOM_RATIO_TAG), - text = stringResource(id = R.string.zoom_scale_text, zoomScale) + text = stringResource(id = R.string.zoom_scale_text, zoomRatio) ) } From 39e0aeb8c61aa578fc01f361c5381c383b59675c Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 11 Dec 2024 17:53:54 +0000 Subject: [PATCH 06/13] persist zoom level between lens changes --- .../core/camera/CameraSession.kt | 95 +++++++++++-------- .../core/camera/CameraSessionSettings.kt | 2 +- .../core/camera/CameraXCameraUseCase.kt | 14 ++- .../settings/model/CameraAppSettings.kt | 2 +- 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index ab07af3d7..4187c7334 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -90,6 +90,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -100,7 +101,8 @@ internal suspend fun runSingleCameraSession( sessionSettings: PerpetualSessionSettings.SingleCamera, useCaseMode: CameraUseCase.UseCaseMode, // TODO(tm): ImageCapture should go through an event channel like VideoCapture - onImageCaptureCreated: (ImageCapture) -> Unit = {} + onImageCaptureCreated: (ImageCapture) -> Unit = {}, + onSetZoomRatio: (Map) -> Unit = { _ -> } ) = coroutineScope { Log.d(TAG, "Starting new single camera session") @@ -173,52 +175,68 @@ internal suspend fun runSingleCameraSession( } } - // update camerastate to mirror current zoomstate - launch { - camera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> - currentCameraState.update { old -> - old.copy( - zoomRatios = old.zoomRatios.toMutableMap().apply { - put(camera.cameraInfo.appLensFacing, zoomState.zoomRatio) - }, - linearZoomScales = old.linearZoomScales.toMutableMap().apply { - put(camera.cameraInfo.appLensFacing, zoomState.linearZoom) - } - ) - } - } - } - - launch { - // Apply camera zoom - zoomChanges.filterNotNull().collectLatest { zoomChange -> - camera.cameraInfo.zoomState.value?.let { currentZoomState -> - when (zoomChange) { - is CameraZoomState.Ratio -> { - camera.cameraControl.setZoomRatio( - zoomChange.value.coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio - ) + // update camerastate to mirror current zoomstate + launch { + camera.cameraInfo.zoomState.asFlow() + .filterNotNull() + .onCompletion { + // save current zoom state to current camera settings when flipping + onSetZoomRatio( + currentCameraState.value.zoomRatios + ) + }.collectLatest { zoomState -> + currentCameraState.update { old -> + old.copy( + zoomRatios = old.zoomRatios.toMutableMap().apply { + put(camera.cameraInfo.appLensFacing, zoomState.zoomRatio) + }, + linearZoomScales = old.linearZoomScales.toMutableMap().apply { + put(camera.cameraInfo.appLensFacing, zoomState.linearZoom) + } ) } + } + } - is CameraZoomState.Linear -> { - camera.cameraControl.setLinearZoom(zoomChange.value) - } + launch { + // Immediately Apply camera zoom from current settings when opening a new camera + camera.cameraControl.setZoomRatio( + currentTransientSettings.zoomRatios[camera.cameraInfo.appLensFacing] ?: 1f + ) + Log.d( + TAG, + "Starting camera ${camera.cameraInfo.appLensFacing} at zoom ratio ${camera.cameraInfo.zoomState.value?.zoomRatio}" + ) - is CameraZoomState.Scale -> { - val newRatio = - (currentZoomState.zoomRatio * zoomChange.value).coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio + // Apply camera zoom changes + zoomChanges.filterNotNull().collectLatest { zoomChange -> + camera.cameraInfo.zoomState.value?.let { currentZoomState -> + when (zoomChange) { + is CameraZoomState.Ratio -> { + camera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) ) - camera.cameraControl.setZoomRatio(newRatio) + } + + is CameraZoomState.Linear -> { + camera.cameraControl.setLinearZoom(zoomChange.value) + } + + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + camera.cameraControl.setZoomRatio(newRatio) + } } } } } - } applyDeviceRotation(currentTransientSettings.deviceRotation, useCaseGroup) processTransientSettingEvents( @@ -256,7 +274,6 @@ internal suspend fun processTransientSettingEvents( val newTransientSettings = it.first val cameraState = it.second - // todo(): How should we handle torch on Auto FlashMode? // enable torch only while recording is in progress if ((cameraState.videoRecordingState !is VideoRecordingState.Inactive) && diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt index 348fecfd4..1cd6fa8cf 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionSettings.kt @@ -62,5 +62,5 @@ internal data class TransientSessionSettings( val deviceRotation: DeviceRotation, val flashMode: FlashMode, val primaryLensFacing: LensFacing, - val zoomRatios: Map + val zoomRatios: Map ) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index e63006467..c4d834df8 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -343,10 +343,16 @@ constructor( when (sessionSettings) { is PerpetualSessionSettings.SingleCamera -> runSingleCameraSession( sessionSettings, - useCaseMode = useCaseMode - ) { imageCapture -> - imageCaptureUseCase = imageCapture - } + useCaseMode = useCaseMode, + onSetZoomRatio = { newZoomRatios -> + currentSettings.update { old -> + old?.copy(defaultZoomRatios = newZoomRatios) + } + }, + onImageCaptureCreated = { imageCapture -> + imageCaptureUseCase = imageCapture + } + ) is PerpetualSessionSettings.ConcurrentCamera -> runConcurrentCameraSession( diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt index df56a854f..58dfa6654 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt @@ -30,7 +30,7 @@ data class CameraAppSettings( val dynamicRange: DynamicRange = DynamicRange.SDR, val defaultHdrDynamicRange: DynamicRange = DynamicRange.HLG10, val defaultHdrImageOutputFormat: ImageOutputFormat = ImageOutputFormat.JPEG_ULTRA_HDR, - val defaultZoomRatios: Map = mapOf(), + val defaultZoomRatios: Map = mapOf(), val targetFrameRate: Int = TARGET_FPS_AUTO, val imageFormat: ImageOutputFormat = ImageOutputFormat.JPEG, val audioMuted: Boolean = false, From f41a4151c7284512d3966033518fcbfddf4c4981 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 11 Dec 2024 22:35:13 +0000 Subject: [PATCH 07/13] WIP: support concurrent zoom persistence --- .../core/camera/CameraSession.kt | 51 +++++----- .../core/camera/CameraXCameraUseCase.kt | 7 +- .../core/camera/ConcurrentCameraSession.kt | 95 ++++++++++++------- 3 files changed, 92 insertions(+), 61 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 4187c7334..c84af4557 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -87,6 +87,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -102,7 +103,7 @@ internal suspend fun runSingleCameraSession( useCaseMode: CameraUseCase.UseCaseMode, // TODO(tm): ImageCapture should go through an event channel like VideoCapture onImageCaptureCreated: (ImageCapture) -> Unit = {}, - onSetZoomRatio: (Map) -> Unit = { _ -> } + onSetZoomRatioMap: (Map) -> Unit = { _ -> } ) = coroutineScope { Log.d(TAG, "Starting new single camera session") @@ -179,9 +180,10 @@ internal suspend fun runSingleCameraSession( launch { camera.cameraInfo.zoomState.asFlow() .filterNotNull() + .distinctUntilChanged() .onCompletion { // save current zoom state to current camera settings when flipping - onSetZoomRatio( + onSetZoomRatioMap( currentCameraState.value.zoomRatios ) }.collectLatest { zoomState -> @@ -209,30 +211,29 @@ internal suspend fun runSingleCameraSession( ) // Apply camera zoom changes - zoomChanges.filterNotNull().collectLatest { zoomChange -> - camera.cameraInfo.zoomState.value?.let { currentZoomState -> - when (zoomChange) { - is CameraZoomState.Ratio -> { - camera.cameraControl.setZoomRatio( - zoomChange.value.coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio - ) + zoomChanges.drop(1).filterNotNull().collectLatest { zoomChange -> + val currentZoomState = camera.cameraInfo.zoomState.asFlow().first() + when (zoomChange) { + is CameraZoomState.Ratio -> { + camera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio ) - } - - is CameraZoomState.Linear -> { - camera.cameraControl.setLinearZoom(zoomChange.value) - } - - is CameraZoomState.Scale -> { - val newRatio = - (currentZoomState.zoomRatio * zoomChange.value).coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio - ) - camera.cameraControl.setZoomRatio(newRatio) - } + ) + } + + is CameraZoomState.Linear -> { + camera.cameraControl.setLinearZoom(zoomChange.value) + } + + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + camera.cameraControl.setZoomRatio(newRatio) } } } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index c4d834df8..75d26c040 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -344,7 +344,7 @@ constructor( is PerpetualSessionSettings.SingleCamera -> runSingleCameraSession( sessionSettings, useCaseMode = useCaseMode, - onSetZoomRatio = { newZoomRatios -> + onSetZoomRatioMap = { newZoomRatios -> currentSettings.update { old -> old?.copy(defaultZoomRatios = newZoomRatios) } @@ -357,6 +357,11 @@ constructor( is PerpetualSessionSettings.ConcurrentCamera -> runConcurrentCameraSession( sessionSettings, + onSetZoomRatioMap = { newZoomRatios -> + currentSettings.update { old -> + old?.copy(defaultZoomRatios = newZoomRatios) + } + }, useCaseMode = CameraUseCase.UseCaseMode.VIDEO_ONLY ) } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index e6c2a62b1..265ba68d3 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -19,15 +19,20 @@ import android.annotation.SuppressLint import android.util.Log import androidx.camera.core.CompositionSettings import androidx.camera.core.TorchState +import androidx.concurrent.futures.await import androidx.lifecycle.asFlow import com.google.jetpackcamera.settings.model.CameraZoomState import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.ImageOutputFormat +import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.StabilizationMode import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -37,6 +42,7 @@ context(CameraSessionContext) @SuppressLint("RestrictedApi") internal suspend fun runConcurrentCameraSession( sessionSettings: PerpetualSessionSettings.ConcurrentCamera, + onSetZoomRatioMap: (Map) -> Unit = { _ -> }, useCaseMode: CameraUseCase.UseCaseMode ) = coroutineScope { val primaryLensFacing = sessionSettings.primaryCameraInfo.appLensFacing @@ -98,7 +104,7 @@ internal suspend fun runConcurrentCameraSession( cameraProvider.runWithConcurrent(cameraConfigs, useCaseGroup) { concurrentCamera -> Log.d(TAG, "Concurrent camera session started") - // a bug? concurrent camera only ever lists one camera + // todo: bug?? concurrent camera only ever lists one camera val primaryCamera = concurrentCamera.cameras.first { it.cameraInfo.appLensFacing == sessionSettings.primaryCameraInfo.appLensFacing } @@ -122,48 +128,67 @@ internal suspend fun runConcurrentCameraSession( } } - // update camerastate to mirror current zoomstate + // update cameraState to mirror the current zoomState launch { - primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().collectLatest { zoomState -> - currentCameraState.update { old -> - old.copy( - zoomRatios = old.zoomRatios.toMutableMap().apply { - put(primaryCamera.cameraInfo.appLensFacing, zoomState.zoomRatio) - }.toMap(), - linearZoomScales = old.linearZoomScales.toMutableMap().apply { - put(primaryCamera.cameraInfo.appLensFacing, zoomState.linearZoom) - }.toMap() + // todo bug? why isn't this catching the initial setZoomRatio? the camerastate zoom is not updating properly + primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().distinctUntilChanged() + .onCompletion { + // save current zoom state to current camera settings when flipping + onSetZoomRatioMap( + currentCameraState.value.zoomRatios ) + }.collectLatest { zoomState -> + currentCameraState.update { old -> + old.copy( + zoomRatios = old.zoomRatios.toMutableMap().apply { + put(primaryCamera.cameraInfo.appLensFacing, zoomState.zoomRatio) + }.toMap(), + linearZoomScales = old.linearZoomScales.toMutableMap().apply { + put(primaryCamera.cameraInfo.appLensFacing, zoomState.linearZoom) + }.toMap() + ) + } } - } } launch { // Apply camera zoom - zoomChanges.filterNotNull().collectLatest { zoomChange -> - primaryCamera.cameraInfo.zoomState.value?.let { currentZoomState -> - when (zoomChange) { - is CameraZoomState.Ratio -> { - primaryCamera.cameraControl.setZoomRatio( - zoomChange.value.coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio - ) + // Immediately Apply camera zoom from current settings when opening a new camera + primaryCamera.cameraControl.setZoomRatio( + initialTransientSettings.zoomRatios[primaryLensFacing] ?: 1f + ).await() + + // todo: what is happening after the first setZoomRatio? The zoom applies but is not reflected in cameraInfo.ZoomState? + // the only ways this works is to call it twice for some reason... somethings going wrong somewhere + primaryCamera.cameraControl.setZoomRatio( + initialTransientSettings.zoomRatios[primaryLensFacing] ?: 1f + ).await() + zoomChanges.drop(1).filterNotNull().collectLatest { zoomChange -> + val currentZoomState = primaryCamera.cameraInfo.zoomState + .asFlow() + .filterNotNull() + .first() + when (zoomChange) { + is CameraZoomState.Ratio -> { + primaryCamera.cameraControl.setZoomRatio( + zoomChange.value.coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio + ) + ).await() + } + + is CameraZoomState.Linear -> { + primaryCamera.cameraControl.setLinearZoom(zoomChange.value).await() + } + + is CameraZoomState.Scale -> { + val newRatio = + (currentZoomState.zoomRatio * zoomChange.value).coerceIn( + currentZoomState.minZoomRatio, + currentZoomState.maxZoomRatio ) - } - - is CameraZoomState.Linear -> { - primaryCamera.cameraControl.setLinearZoom(zoomChange.value) - } - - is CameraZoomState.Scale -> { - val newRatio = - (currentZoomState.zoomRatio * zoomChange.value).coerceIn( - currentZoomState.minZoomRatio, - currentZoomState.maxZoomRatio - ) - primaryCamera.cameraControl.setZoomRatio(newRatio) - } + primaryCamera.cameraControl.setZoomRatio(newRatio).await() } } } From d33b0c56afc2c15d1da8974922a0c356c8fa171c Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 11 Dec 2024 23:22:47 +0000 Subject: [PATCH 08/13] comment --- .../com/google/jetpackcamera/core/camera/CameraSession.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index c84af4557..bf79f58c2 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -178,11 +178,17 @@ internal suspend fun runSingleCameraSession( // update camerastate to mirror current zoomstate launch { + /* + TODO bug?? Flaky behavior here. does not always update zoomstate properly when: + switching HDR on or off on front lens + Setting aspect ratio on either lens... + basically anything that restarts the session + */ camera.cameraInfo.zoomState.asFlow() .filterNotNull() .distinctUntilChanged() .onCompletion { - // save current zoom state to current camera settings when flipping + // save current zoom state to current camera settings when changing cameras onSetZoomRatioMap( currentCameraState.value.zoomRatios ) From a23bc355ec9d7838a075261e661772a13db50383 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 18 Dec 2024 18:29:00 +0000 Subject: [PATCH 09/13] spotless --- .../core/camera/CameraXCameraUseCase.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index d378a3bde..99f6b7370 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -24,6 +24,7 @@ import android.os.Environment import android.provider.MediaStore import android.util.Log import androidx.camera.core.CameraInfo +import androidx.camera.core.DynamicRange as CXDynamicRange import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture.OutputFileOptions import androidx.camera.core.ImageCaptureException @@ -55,6 +56,13 @@ import com.google.jetpackcamera.settings.model.StabilizationMode import com.google.jetpackcamera.settings.model.SystemConstraints import com.google.jetpackcamera.settings.model.forCurrentLens import dagger.hilt.android.scopes.ViewModelScoped +import java.io.File +import java.io.FileNotFoundException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import javax.inject.Inject +import kotlin.properties.Delegates import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope @@ -67,14 +75,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileNotFoundException -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale -import javax.inject.Inject -import kotlin.properties.Delegates -import androidx.camera.core.DynamicRange as CXDynamicRange private const val TAG = "CameraXCameraUseCase" const val TARGET_FPS_AUTO = 0 From 1de2822fc64499eff629dc9fcc410fb8eedf2c7f Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 21 Jan 2025 14:57:37 +0000 Subject: [PATCH 10/13] spotless --- .../jetpackcamera/core/camera/CameraSession.kt | 12 ++++++------ .../core/camera/CameraXCameraUseCase.kt | 16 ++++++++-------- .../feature/preview/PreviewViewModel.kt | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 14a7c44c5..376cdd2ce 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -82,6 +82,12 @@ import com.google.jetpackcamera.settings.model.VideoQuality.FHD import com.google.jetpackcamera.settings.model.VideoQuality.HD import com.google.jetpackcamera.settings.model.VideoQuality.SD import com.google.jetpackcamera.settings.model.VideoQuality.UHD +import java.io.File +import java.util.Date +import java.util.concurrent.Executor +import kotlin.coroutines.ContinuationInterceptor +import kotlin.math.abs +import kotlin.time.Duration.Companion.milliseconds import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.asExecutor @@ -100,12 +106,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.io.File -import java.util.Date -import java.util.concurrent.Executor -import kotlin.coroutines.ContinuationInterceptor -import kotlin.math.abs -import kotlin.time.Duration.Companion.milliseconds private const val TAG = "CameraSession" private val QUALITY_RANGE_MAP = mapOf( diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index 2f501b807..cfd8b3d3c 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -24,6 +24,7 @@ import android.os.Environment import android.provider.MediaStore import android.util.Log import androidx.camera.core.CameraInfo +import androidx.camera.core.DynamicRange as CXDynamicRange import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture.OutputFileOptions import androidx.camera.core.ImageCaptureException @@ -56,6 +57,13 @@ import com.google.jetpackcamera.settings.model.SystemConstraints import com.google.jetpackcamera.settings.model.VideoQuality import com.google.jetpackcamera.settings.model.forCurrentLens import dagger.hilt.android.scopes.ViewModelScoped +import java.io.File +import java.io.FileNotFoundException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import javax.inject.Inject +import kotlin.properties.Delegates import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope @@ -68,14 +76,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileNotFoundException -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale -import javax.inject.Inject -import kotlin.properties.Delegates -import androidx.camera.core.DynamicRange as CXDynamicRange private const val TAG = "CameraXCameraUseCase" const val TARGET_FPS_AUTO = 0 diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index 0167f993d..34d2596d8 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -57,6 +57,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel +import kotlin.reflect.KProperty +import kotlin.reflect.full.memberProperties +import kotlin.time.Duration.Companion.seconds import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred @@ -73,9 +76,6 @@ import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlin.reflect.KProperty -import kotlin.reflect.full.memberProperties -import kotlin.time.Duration.Companion.seconds private const val TAG = "PreviewViewModel" private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture" From 6754617ab3dbdc63dc723b3c03965b769a007ca8 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 21 Jan 2025 16:00:12 +0000 Subject: [PATCH 11/13] reset certain settings to pre-recording state after capture --- .../core/camera/CameraSession.kt | 34 +++++++++++++------ .../core/camera/CameraXCameraUseCase.kt | 29 ++++++++++++++-- .../core/camera/VideoCaptureControlEvent.kt | 1 + .../settings/model/CameraAppSettings.kt | 1 - 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 376cdd2ce..7581b258e 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -231,11 +231,16 @@ internal suspend fun runSingleCameraSession( .filterNotNull() .distinctUntilChanged() .onCompletion { - // save current zoom state to current camera settings when changing cameras - onSetZoomRatioMap( - currentCameraState.value.zoomRatios - ) - }.collectLatest { zoomState -> + // reset current camera state when changing cameras. + currentCameraState.update { + old -> + old.copy( + zoomRatios = emptyMap(), + linearZoomScales = emptyMap() + ) + } + } + .collectLatest { zoomState -> currentCameraState.update { old -> old.copy( zoomRatios = old.zoomRatios.toMutableMap().apply { @@ -246,6 +251,10 @@ internal suspend fun runSingleCameraSession( } ) } + //update current settings to mirror current camera state + onSetZoomRatioMap( + currentCameraState.value.zoomRatios + ) } } @@ -259,7 +268,7 @@ internal suspend fun runSingleCameraSession( "Starting camera ${camera.cameraInfo.appLensFacing} at zoom ratio ${camera.cameraInfo.zoomState.value?.zoomRatio}" ) - // Apply camera zoom changes + // Apply zoom changes to camera zoomChanges.drop(1).filterNotNull().collectLatest { zoomChange -> val currentZoomState = camera.cameraInfo.zoomState.asFlow().first() when (zoomChange) { @@ -756,7 +765,8 @@ private suspend fun startVideoRecordingInternal( context: Context, pendingRecord: PendingRecording, maxDurationMillis: Long, - onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit + onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit, + onRestoreSettings: () -> Unit = {}, ): Recording { Log.d(TAG, "recordVideo") // todo(b/336886716): default setting to enable or disable audio when permission is granted @@ -876,6 +886,7 @@ private suspend fun startVideoRecordingInternal( onVideoRecordEvent.outputResults.outputUri ) ) + onRestoreSettings() } ERROR_DURATION_LIMIT_REACHED -> { @@ -931,7 +942,8 @@ private suspend fun runVideoRecording( videoCaptureUri: Uri?, videoControlEvents: Channel, shouldUseUri: Boolean, - onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit + onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit, + onRestoreSettings: () -> Unit = {}, ) = coroutineScope { var currentSettings = transientSettings.filterNotNull().first() @@ -949,7 +961,8 @@ private suspend fun runVideoRecording( context = context, pendingRecord = it, maxDurationMillis = maxDurationMillis, - onVideoRecord = onVideoRecord + onVideoRecord = onVideoRecord, + onRestoreSettings = onRestoreSettings ).use { recording -> val recordingSettingsUpdater = launch { fun TransientSessionSettings.isFlashModeOn() = flashMode == FlashMode.ON @@ -1032,7 +1045,8 @@ internal suspend fun processVideoControlEvents( event.videoCaptureUri, videoCaptureControlEvents, event.shouldUseUri, - event.onVideoRecord + event.onVideoRecord, + event.onRestoreSettings ) } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index cfd8b3d3c..da7976051 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -377,7 +377,11 @@ constructor( useCaseMode = useCaseMode, onSetZoomRatioMap = { newZoomRatios -> currentSettings.update { old -> - old?.copy(defaultZoomRatios = newZoomRatios) + old?.copy(defaultZoomRatios = + old.defaultZoomRatios.toMutableMap().apply { + putAll(newZoomRatios) + } + ) } }, onImageCaptureCreated = { imageCapture -> @@ -538,6 +542,7 @@ constructor( shouldUseUri: Boolean, onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit ) { + val initialRecordSettings = currentSettings.value if (shouldUseUri && videoCaptureUri == null) { val e = RuntimeException("Null Uri is provided.") Log.d(TAG, "takePicture onError: $e") @@ -549,7 +554,27 @@ constructor( shouldUseUri, currentSettings.value?.maxVideoDurationMillis ?: UNLIMITED_VIDEO_DURATION, - onVideoRecord + onVideoRecord = onVideoRecord, + onRestoreSettings = { + Log.d(TAG, "INITIAL RECORD SETTINGS: $initialRecordSettings") + + Log.d(TAG, "CURRENT RECORD SETTINGS: ${currentSettings.value}") + + initialRecordSettings?.let { + currentSettings.update { old -> + old?.copy( + cameraLensFacing = initialRecordSettings.cameraLensFacing, + defaultZoomRatios = initialRecordSettings.defaultZoomRatios, + audioMuted = initialRecordSettings.audioMuted, + + ) + } + + //zoom needs to be manually reset on the current lens since it doesn't read updates directly from the settings + zoomChanges.update { CameraZoomState.Ratio( + initialRecordSettings.defaultZoomRatios[initialRecordSettings.cameraLensFacing]?:1f) } + } + } ) ) } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt index 1132d8e2f..cdd35a2a4 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/VideoCaptureControlEvent.kt @@ -31,6 +31,7 @@ sealed interface VideoCaptureControlEvent { val videoCaptureUri: Uri?, val shouldUseUri: Boolean, val maxVideoDuration: Long, + val onRestoreSettings: () -> Unit, val onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit ) : VideoCaptureControlEvent diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt index 0b2b3b773..19e3107b1 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt @@ -32,7 +32,6 @@ data class CameraAppSettings( val stabilizationMode: StabilizationMode = StabilizationMode.AUTO, val dynamicRange: DynamicRange = DynamicRange.SDR, val videoQuality: VideoQuality = VideoQuality.UNSPECIFIED, - val zoomScale: Float = 1f, val defaultZoomRatios: Map = mapOf(), val targetFrameRate: Int = TARGET_FPS_AUTO, val imageFormat: ImageOutputFormat = ImageOutputFormat.JPEG, From 38f3dba611ea6ea8fc9dbe5bfa2c5a74f4bb3a6d Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Tue, 21 Jan 2025 16:21:02 +0000 Subject: [PATCH 12/13] spotless --- .../core/camera/CameraSession.kt | 8 ++--- .../core/camera/CameraXCameraUseCase.kt | 31 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 7581b258e..86c0fdec6 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -233,7 +233,7 @@ internal suspend fun runSingleCameraSession( .onCompletion { // reset current camera state when changing cameras. currentCameraState.update { - old -> + old -> old.copy( zoomRatios = emptyMap(), linearZoomScales = emptyMap() @@ -251,7 +251,7 @@ internal suspend fun runSingleCameraSession( } ) } - //update current settings to mirror current camera state + // update current settings to mirror current camera state onSetZoomRatioMap( currentCameraState.value.zoomRatios ) @@ -766,7 +766,7 @@ private suspend fun startVideoRecordingInternal( pendingRecord: PendingRecording, maxDurationMillis: Long, onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit, - onRestoreSettings: () -> Unit = {}, + onRestoreSettings: () -> Unit = {} ): Recording { Log.d(TAG, "recordVideo") // todo(b/336886716): default setting to enable or disable audio when permission is granted @@ -943,7 +943,7 @@ private suspend fun runVideoRecording( videoControlEvents: Channel, shouldUseUri: Boolean, onVideoRecord: (CameraUseCase.OnVideoRecordEvent) -> Unit, - onRestoreSettings: () -> Unit = {}, + onRestoreSettings: () -> Unit = {} ) = coroutineScope { var currentSettings = transientSettings.filterNotNull().first() diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index da7976051..1767fdb7c 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -377,11 +377,12 @@ constructor( useCaseMode = useCaseMode, onSetZoomRatioMap = { newZoomRatios -> currentSettings.update { old -> - old?.copy(defaultZoomRatios = + old?.copy( + defaultZoomRatios = old.defaultZoomRatios.toMutableMap().apply { putAll(newZoomRatios) } - ) + ) } }, onImageCaptureCreated = { imageCapture -> @@ -555,24 +556,29 @@ constructor( currentSettings.value?.maxVideoDurationMillis ?: UNLIMITED_VIDEO_DURATION, onVideoRecord = onVideoRecord, - onRestoreSettings = { - Log.d(TAG, "INITIAL RECORD SETTINGS: $initialRecordSettings") - - Log.d(TAG, "CURRENT RECORD SETTINGS: ${currentSettings.value}") + onRestoreSettings = { + // restore settings to be called after video recording completes. + // this resets certain settings to their values pre-recording initialRecordSettings?.let { currentSettings.update { old -> old?.copy( cameraLensFacing = initialRecordSettings.cameraLensFacing, defaultZoomRatios = initialRecordSettings.defaultZoomRatios, - audioMuted = initialRecordSettings.audioMuted, + audioMuted = initialRecordSettings.audioMuted ) } - //zoom needs to be manually reset on the current lens since it doesn't read updates directly from the settings - zoomChanges.update { CameraZoomState.Ratio( - initialRecordSettings.defaultZoomRatios[initialRecordSettings.cameraLensFacing]?:1f) } + // if the lens doesnt flip when restoring settings, the zoom will need to be + // manually reapplied since zoom changes are processed through state changes to + // zoomchanges and not settings + zoomChanges.update { + CameraZoomState.Ratio( + initialRecordSettings + .defaultZoomRatios[initialRecordSettings.cameraLensFacing] ?: 1f + ) + } } } ) @@ -700,8 +706,8 @@ constructor( } } - private fun CameraAppSettings.tryApplyVideoQualityConstraints(): CameraAppSettings { - return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + private fun CameraAppSettings.tryApplyVideoQualityConstraints(): CameraAppSettings = + systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> with(constraints.supportedVideoQualitiesMap) { val newVideoQuality = get(dynamicRange).let { if (it == null) { @@ -718,7 +724,6 @@ constructor( ) } } ?: this - } private fun CameraAppSettings.tryApplyFlashModeConstraints(): CameraAppSettings = systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> From 59fa5da9dde646e510bb6d7a2986d78dbfaa1e54 Mon Sep 17 00:00:00 2001 From: Kimberly Crevecoeur Date: Wed, 22 Jan 2025 17:29:01 +0000 Subject: [PATCH 13/13] maintain zoom ratio when switching lenses in concurrent mode --- .../core/camera/CameraSession.kt | 18 ++++++++-------- .../core/camera/CameraXCameraUseCase.kt | 7 ++++++- .../core/camera/ConcurrentCameraSession.kt | 21 ++++++++----------- .../jetpackcamera/settings/SettingsUiState.kt | 3 +-- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 86c0fdec6..267416d4e 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -232,8 +232,7 @@ internal suspend fun runSingleCameraSession( .distinctUntilChanged() .onCompletion { // reset current camera state when changing cameras. - currentCameraState.update { - old -> + currentCameraState.update { old -> old.copy( zoomRatios = emptyMap(), linearZoomScales = emptyMap() @@ -245,10 +244,10 @@ internal suspend fun runSingleCameraSession( old.copy( zoomRatios = old.zoomRatios.toMutableMap().apply { put(camera.cameraInfo.appLensFacing, zoomState.zoomRatio) - }, + }.toMap(), linearZoomScales = old.linearZoomScales.toMutableMap().apply { put(camera.cameraInfo.appLensFacing, zoomState.linearZoom) - } + }.toMap() ) } // update current settings to mirror current camera state @@ -265,12 +264,14 @@ internal suspend fun runSingleCameraSession( ) Log.d( TAG, - "Starting camera ${camera.cameraInfo.appLensFacing} at zoom ratio ${camera.cameraInfo.zoomState.value?.zoomRatio}" + "Starting camera ${camera.cameraInfo.appLensFacing} at zoom ratio " + + "${camera.cameraInfo.zoomState.value?.zoomRatio}" ) // Apply zoom changes to camera zoomChanges.drop(1).filterNotNull().collectLatest { zoomChange -> - val currentZoomState = camera.cameraInfo.zoomState.asFlow().first() + val currentZoomState = camera.cameraInfo.zoomState + .asFlow().filterNotNull().first() when (zoomChange) { is CameraZoomState.Ratio -> { camera.cameraControl.setZoomRatio( @@ -481,13 +482,12 @@ internal fun createUseCaseGroup( }.build() } -private fun getVideoQualityFromResolution(resolution: Size?): VideoQuality { - return resolution?.let { res -> +private fun getVideoQualityFromResolution(resolution: Size?): VideoQuality = + resolution?.let { res -> QUALITY_RANGE_MAP.firstNotNullOfOrNull { if (it.value.contains(res.height)) it.key else null } } ?: VideoQuality.UNSPECIFIED -} private fun getWidthFromCropRect(cropRect: Rect?): Int { if (cropRect == null) { diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt index 1767fdb7c..f42809b84 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraUseCase.kt @@ -395,7 +395,12 @@ constructor( sessionSettings, onSetZoomRatioMap = { newZoomRatios -> currentSettings.update { old -> - old?.copy(defaultZoomRatios = newZoomRatios) + old?.copy( + defaultZoomRatios = + old.defaultZoomRatios.toMutableMap().apply { + putAll(newZoomRatios) + } + ) } }, useCaseMode = CameraUseCase.UseCaseMode.VIDEO_ONLY diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt index 9449c6c27..24bb3d133 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/ConcurrentCameraSession.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -134,12 +133,7 @@ internal suspend fun runConcurrentCameraSession( launch { // todo bug? why isn't this catching the initial setZoomRatio? the camerastate zoom is not updating properly primaryCamera.cameraInfo.zoomState.asFlow().filterNotNull().distinctUntilChanged() - .onCompletion { - // save current zoom state to current camera settings when flipping - onSetZoomRatioMap( - currentCameraState.value.zoomRatios - ) - }.collectLatest { zoomState -> + .collectLatest { zoomState -> currentCameraState.update { old -> old.copy( zoomRatios = old.zoomRatios.toMutableMap().apply { @@ -150,17 +144,20 @@ internal suspend fun runConcurrentCameraSession( }.toMap() ) } + // update current settings to mirror current camera state + onSetZoomRatioMap( + currentCameraState.value.zoomRatios + ) } } launch { - // Apply camera zoom // Immediately Apply camera zoom from current settings when opening a new camera primaryCamera.cameraControl.setZoomRatio( initialTransientSettings.zoomRatios[primaryLensFacing] ?: 1f ).await() - // todo: what is happening after the first setZoomRatio? The zoom applies but is not reflected in cameraInfo.ZoomState? + // todo: what is happening after the first setZoomRatio? The initial setzoom applies but is not reflected in cameraInfo.ZoomState? // the only ways this works is to call it twice for some reason... somethings going wrong somewhere primaryCamera.cameraControl.setZoomRatio( initialTransientSettings.zoomRatios[primaryLensFacing] ?: 1f @@ -177,11 +174,11 @@ internal suspend fun runConcurrentCameraSession( currentZoomState.minZoomRatio, currentZoomState.maxZoomRatio ) - ).await() + ) } is CameraZoomState.Linear -> { - primaryCamera.cameraControl.setLinearZoom(zoomChange.value).await() + primaryCamera.cameraControl.setLinearZoom(zoomChange.value) } is CameraZoomState.Scale -> { @@ -190,7 +187,7 @@ internal suspend fun runConcurrentCameraSession( currentZoomState.minZoomRatio, currentZoomState.maxZoomRatio ) - primaryCamera.cameraControl.setZoomRatio(newRatio).await() + primaryCamera.cameraControl.setZoomRatio(newRatio) } } } diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt index f4934ccc7..bdd422013 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsUiState.kt @@ -95,8 +95,7 @@ sealed interface DisabledRationale { data class VideoQualityUnsupportedRationale( override val affectedSettingNameResId: Int, val currentDynamicRange: Int = R.string.video_quality_rationale_suffix_default - ) : - DisabledRationale { + ) : DisabledRationale { override val reasonTextResId = R.string.video_quality_unsupported override val testTag = VIDEO_QUALITY_UNSUPPORTED_TAG }