Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,8 @@ This feature offers multiple stabilization modes to enhance video smoothness and
following stabilization modes are offered as options by JCA

* **Auto:** Automatically enables stabilization based on the device's capabilities and current
recording settings.
recording settings. In image-only capture mode, “Auto” uses optical stabilization if the lens
supports it and does not turn on preview stabilization, which crops the captured image.
* **On:** Activates stabilization for both the preview and recorded video streams, providing a
smooth viewing experience during capture and playback.
* **High Quality:** Applies a high-quality stabilization algorithm to the recorded video stream,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,54 @@ class CameraXCameraSystemTest {
runSetStabilizationModeTest(StabilizationMode.OFF, CaptureMode.IMAGE_ONLY)
}

@Test
fun setStabilizationMode_on_imageOnly_updatesCameraState(): Unit = runBlocking {
runSetStabilizationModeTest(StabilizationMode.ON, CaptureMode.IMAGE_ONLY)
}

@Test
fun setStabilizationMode_optical_imageOnly_updatesCameraState(): Unit = runBlocking {
runSetStabilizationModeTest(StabilizationMode.OPTICAL, CaptureMode.IMAGE_ONLY)
}

@Test
fun autoStabilization_imageOnly_doesNotUsePreviewStabilization(): Unit = runBlocking {
// Arrange.
val cameraSystem = createAndInitCameraXCameraSystem(
appSettings = CameraAppSettings(stabilizationMode = StabilizationMode.AUTO)
)
val supportedStabilizationModes =
cameraSystem.getSystemConstraints().value?.forCurrentLens(
DEFAULT_CAMERA_APP_SETTINGS
)?.supportedStabilizationModes
assume().withMessage("Preview stabilization not supported, skip the test.")
.that(supportedStabilizationModes?.contains(StabilizationMode.ON)).isTrue()
val expectedImageOnlyMode =
if (supportedStabilizationModes?.contains(StabilizationMode.OPTICAL) == true) {
StabilizationMode.OPTICAL
} else {
StabilizationMode.OFF
}

cameraSystem.startCameraAndWaitUntilRunning()
val stabilizationCheck: ReceiveChannel<StabilizationMode> =
cameraSystem.getCurrentCameraState()
.map { it.stabilizationMode }
.produceIn(this)

// Ensure AUTO resolves to preview stabilization while video can be recorded
stabilizationCheck.awaitValue(StabilizationMode.ON)

// Act.
cameraSystem.setCaptureMode(CaptureMode.IMAGE_ONLY)

// Assert.
stabilizationCheck.awaitValue(expectedImageOnlyMode)

// Clean-up.
stabilizationCheck.cancel()
}

private suspend fun CoroutineScope.runSetStabilizationModeTest(
stabilizationMode: StabilizationMode,
captureMode: CaptureMode? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,13 @@ private fun createPreviewUseCase(
}
when (effectiveStabilizationMode) {
StabilizationMode.ON -> setPreviewStabilizationEnabled(true)
StabilizationMode.OPTICAL -> setOpticalStabilizationModeEnabled(true)
StabilizationMode.OPTICAL -> {
setOpticalStabilizationModeEnabled(true)
// Explicitly disable preview and video stabilization. If left unset, the camera's
// default applies, which can turn on video stabilization when there isn't a video
// capture use case attached, such as in IMAGE_ONLY mode
setPreviewStabilizationEnabled(false)
}
StabilizationMode.OFF -> {
setOpticalStabilizationModeEnabled(false)
// Setting this to false in Preview use case will disable video stabilization, even in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ class CameraXCameraSystem(

val resolvedStabilizationMode = resolveStabilizationMode(
requestedStabilizationMode = currentCameraSettings.stabilizationMode,
captureMode = currentCameraSettings.captureMode,
targetFrameRate = currentCameraSettings.targetFrameRate,
cameraConstraints = cameraConstraints
)
Expand Down Expand Up @@ -729,29 +730,6 @@ class CameraXCameraSystem(
}
}

private fun resolveStabilizationMode(
requestedStabilizationMode: StabilizationMode,
targetFrameRate: Int,
cameraConstraints: CameraConstraints
): StabilizationMode = with(cameraConstraints) {
// Convert AUTO stabilization mode to the first supported stabilization mode
val stabilizationMode = if (requestedStabilizationMode == StabilizationMode.AUTO) {
// Choose between ON, OPTICAL, or OFF, depending on support, in that order
sequenceOf(StabilizationMode.ON, StabilizationMode.OPTICAL, StabilizationMode.OFF)
.first {
it in supportedStabilizationModes &&
targetFrameRate !in it.unsupportedFpsSet
}
} else {
requestedStabilizationMode
}

// Check that the stabilization mode can be supported, otherwise return OFF
stabilizationMode.takeIf {
it in supportedStabilizationModes && targetFrameRate !in it.unsupportedFpsSet
} ?: StabilizationMode.OFF
}

override suspend fun takePicture(onCaptureStarted: (() -> Unit)) {
val imageCaptureUseCase = boundImageCapture?.useCase
?: throw RuntimeException("Attempted take picture with null imageCapture use case")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2026 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.core.camera

import com.google.jetpackcamera.model.CaptureMode
import com.google.jetpackcamera.model.StabilizationMode
import com.google.jetpackcamera.settings.model.CameraConstraints

/**
* Resolves [requestedStabilizationMode] to the [StabilizationMode] to apply to a camera session.
*
* [StabilizationMode.AUTO] resolves to the first mode in the following order that
* [cameraConstraints] supports at [targetFrameRate]:
* - In [CaptureMode.IMAGE_ONLY]: [StabilizationMode.OPTICAL], then [StabilizationMode.OFF].
* Preview stabilization ([StabilizationMode.ON]) is intended for video and crops the field of
* view, so it is not chosen automatically when no video can be recorded.
* - In other capture modes: [StabilizationMode.ON], [StabilizationMode.OPTICAL], then
* [StabilizationMode.OFF].
*
* Any other requested mode is returned unchanged if it is supported at [targetFrameRate], and
* resolves to [StabilizationMode.OFF] otherwise. This includes [StabilizationMode.ON] in
* [CaptureMode.IMAGE_ONLY], so preview stabilization can still be selected explicitly.
*/
internal fun resolveStabilizationMode(
requestedStabilizationMode: StabilizationMode,
captureMode: CaptureMode,
targetFrameRate: Int,
cameraConstraints: CameraConstraints
): StabilizationMode = with(cameraConstraints) {
fun StabilizationMode.isSupported(): Boolean =
this in supportedStabilizationModes && targetFrameRate !in unsupportedFpsSet

if (requestedStabilizationMode == StabilizationMode.AUTO) {
val autoCandidates = if (captureMode == CaptureMode.IMAGE_ONLY) {
listOf(StabilizationMode.OPTICAL)
} else {
listOf(StabilizationMode.ON, StabilizationMode.OPTICAL)
}
autoCandidates.firstOrNull { it.isSupported() } ?: StabilizationMode.OFF
} else {
requestedStabilizationMode.takeIf { it.isSupported() } ?: StabilizationMode.OFF
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
* Copyright (C) 2026 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.core.camera

import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.model.CaptureMode
import com.google.jetpackcamera.model.StabilizationMode
import com.google.jetpackcamera.model.TARGET_FPS_60
import com.google.jetpackcamera.model.TARGET_FPS_AUTO
import com.google.jetpackcamera.settings.model.CameraConstraints
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class StabilizationModeResolutionTest {

@Test
fun auto_standard_previewAndOpticalSupported_resolvesToOn() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OPTICAL,
StabilizationMode.OFF
)

assertThat(resolve(StabilizationMode.AUTO, CaptureMode.STANDARD, constraints))
.isEqualTo(StabilizationMode.ON)
}

@Test
fun auto_videoOnly_previewAndOpticalSupported_resolvesToOn() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OPTICAL,
StabilizationMode.OFF
)

assertThat(resolve(StabilizationMode.AUTO, CaptureMode.VIDEO_ONLY, constraints))
.isEqualTo(StabilizationMode.ON)
}

@Test
fun auto_standard_onlyOpticalSupported_resolvesToOptical() {
val constraints = cameraConstraints(StabilizationMode.OPTICAL, StabilizationMode.OFF)

assertThat(resolve(StabilizationMode.AUTO, CaptureMode.STANDARD, constraints))
.isEqualTo(StabilizationMode.OPTICAL)
}

@Test
fun auto_standard_noStabilizationSupported_resolvesToOff() {
val constraints = cameraConstraints(StabilizationMode.OFF)

assertThat(resolve(StabilizationMode.AUTO, CaptureMode.STANDARD, constraints))
.isEqualTo(StabilizationMode.OFF)
}

@Test
fun auto_standard_previewUnsupportedAtFrameRate_resolvesToOptical() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OPTICAL,
StabilizationMode.OFF,
unsupportedStabilizationFpsMap = mapOf(StabilizationMode.ON to setOf(TARGET_FPS_60))
)

assertThat(
resolve(
StabilizationMode.AUTO,
CaptureMode.STANDARD,
constraints,
targetFrameRate = TARGET_FPS_60
)
).isEqualTo(StabilizationMode.OPTICAL)
}

@Test
fun auto_imageOnly_previewAndOpticalSupported_resolvesToOptical() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OPTICAL,
StabilizationMode.OFF
)

assertThat(resolve(StabilizationMode.AUTO, CaptureMode.IMAGE_ONLY, constraints))
.isEqualTo(StabilizationMode.OPTICAL)
}

@Test
fun auto_imageOnly_onlyPreviewSupported_resolvesToOff() {
val constraints = cameraConstraints(StabilizationMode.ON, StabilizationMode.OFF)

assertThat(resolve(StabilizationMode.AUTO, CaptureMode.IMAGE_ONLY, constraints))
.isEqualTo(StabilizationMode.OFF)
}

@Test
fun auto_imageOnly_opticalUnsupportedAtFrameRate_resolvesToOff() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OPTICAL,
StabilizationMode.OFF,
unsupportedStabilizationFpsMap = mapOf(
StabilizationMode.OPTICAL to setOf(TARGET_FPS_60)
)
)

assertThat(
resolve(
StabilizationMode.AUTO,
CaptureMode.IMAGE_ONLY,
constraints,
targetFrameRate = TARGET_FPS_60
)
).isEqualTo(StabilizationMode.OFF)
}

@Test
fun on_imageOnly_previewSupported_resolvesToOn() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OPTICAL,
StabilizationMode.OFF
)

assertThat(resolve(StabilizationMode.ON, CaptureMode.IMAGE_ONLY, constraints))
.isEqualTo(StabilizationMode.ON)
}

@Test
fun highQuality_imageOnly_supported_resolvesToHighQuality() {
val constraints = cameraConstraints(StabilizationMode.HIGH_QUALITY, StabilizationMode.OFF)

assertThat(resolve(StabilizationMode.HIGH_QUALITY, CaptureMode.IMAGE_ONLY, constraints))
.isEqualTo(StabilizationMode.HIGH_QUALITY)
}

@Test
fun explicitMode_unsupportedByLens_resolvesToOff() {
val constraints = cameraConstraints(StabilizationMode.OPTICAL, StabilizationMode.OFF)

assertThat(resolve(StabilizationMode.ON, CaptureMode.STANDARD, constraints))
.isEqualTo(StabilizationMode.OFF)
}

@Test
fun explicitMode_unsupportedAtFrameRate_resolvesToOff() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OFF,
unsupportedStabilizationFpsMap = mapOf(StabilizationMode.ON to setOf(TARGET_FPS_60))
)

assertThat(
resolve(
StabilizationMode.ON,
CaptureMode.STANDARD,
constraints,
targetFrameRate = TARGET_FPS_60
)
).isEqualTo(StabilizationMode.OFF)
}

@Test
fun off_resolvesToOff() {
val constraints = cameraConstraints(
StabilizationMode.ON,
StabilizationMode.OPTICAL,
StabilizationMode.OFF
)

assertThat(resolve(StabilizationMode.OFF, CaptureMode.STANDARD, constraints))
.isEqualTo(StabilizationMode.OFF)
}

private fun resolve(
requestedStabilizationMode: StabilizationMode,
captureMode: CaptureMode,
cameraConstraints: CameraConstraints,
targetFrameRate: Int = TARGET_FPS_AUTO
): StabilizationMode = resolveStabilizationMode(
requestedStabilizationMode = requestedStabilizationMode,
captureMode = captureMode,
targetFrameRate = targetFrameRate,
cameraConstraints = cameraConstraints
)

private fun cameraConstraints(
vararg supportedStabilizationModes: StabilizationMode,
unsupportedStabilizationFpsMap: Map<StabilizationMode, Set<Int>> = emptyMap()
) = CameraConstraints(
supportedStabilizationModes = supportedStabilizationModes.toSet(),
supportedFixedFrameRates = emptySet(),
supportedDynamicRanges = emptySet(),
supportedVideoQualitiesMap = emptyMap(),
supportedImageFormatsMap = emptyMap(),
supportedIlluminants = emptySet(),
supportedFlashModes = emptySet(),
supportedZoomRange = null,
unsupportedStabilizationFpsMap = unsupportedStabilizationFpsMap,
supportedTestPatterns = emptySet()
)
}
Loading