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
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

private const val TAG = "CameraSession"

// Ranges are matched against the short edge of the video resolution (orientation-invariant).
// FHD intentionally extends up to 2159 so intermediate resolutions like 1440p (QHD) map to FHD
// rather than falling into a gap and resolving to UNSPECIFIED.
private val QUALITY_RANGE_MAP = mapOf(
UHD to Range.create(2160, 4319),
FHD to Range.create(1080, 1439),
FHD to Range.create(1080, 2159),
Comment thread
davidjiagoogle marked this conversation as resolved.
Resolved
HD to Range.create(720, 1079),
SD to Range.create(241, 719)
)
Expand Down Expand Up @@ -326,27 +330,36 @@ internal suspend fun runSingleCameraSession(
}
}

applyDeviceRotation(currentTransientSettings.deviceRotation, useCaseGroup)

if (videoCaptureUseCase != null) {
val resolutionInfo = videoCaptureUseCase.resolutionInfo
val videoQuality = getVideoQualityFromResolution(
videoCaptureUseCase.resolutionInfo?.resolution
resolutionInfo?.resolution
)
if (videoQuality != sessionSettings.videoQuality) {
Log.e(
if (resolutionInfo != null &&
sessionSettings.videoQuality != VideoQuality.UNSPECIFIED &&
videoQuality != sessionSettings.videoQuality
) {
Comment thread
davidjiagoogle marked this conversation as resolved.
Resolved
Log.w(
TAG,
"Failed to select video quality: " +
"$sessionSettings.videoQuality. Fallback: $videoQuality"
"Requested video quality ${sessionSettings.videoQuality} " +
"unavailable. Fallback: $videoQuality"
)
}
val rotationDegrees = resolutionInfo?.rotationDegrees ?: 0
launch {
currentCameraState.update { old ->
old.copy(
videoQualityInfo = VideoQualityInfo(
videoQuality,
getWidthFromCropRect(
videoCaptureUseCase.resolutionInfo?.cropRect
resolutionInfo?.cropRect,
rotationDegrees
),
getHeightFromCropRect(
videoCaptureUseCase.resolutionInfo?.cropRect
resolutionInfo?.cropRect,
rotationDegrees
)
)
)
Expand Down Expand Up @@ -451,7 +464,6 @@ internal suspend fun runSingleCameraSession(
}
}

applyDeviceRotation(currentTransientSettings.deviceRotation, useCaseGroup)
processTransientSettingEvents(
camera,
cameraConstraints,
Expand Down Expand Up @@ -578,6 +590,21 @@ internal suspend fun processTransientSettingEvents(
"${newTransientSettings.deviceRotation}"
)
applyDeviceRotation(newTransientSettings.deviceRotation, useCaseGroup)
if (sessionSettings != null) {
useCaseGroup.getVideoCapture()?.let { videoCapture ->
val resolutionInfo = videoCapture.resolutionInfo
val rotationDegrees = resolutionInfo?.rotationDegrees ?: 0
currentCameraState.update { old ->
old.copy(
videoQualityInfo = VideoQualityInfo(
getVideoQualityFromResolution(resolutionInfo?.resolution),
getWidthFromCropRect(resolutionInfo?.cropRect, rotationDegrees),
getHeightFromCropRect(resolutionInfo?.cropRect, rotationDegrees)
)
)
}
}
}
}

// setzoomratio when the primary zoom value changes.
Expand Down Expand Up @@ -780,25 +807,46 @@ internal fun createUseCaseGroup(
}.build()
}

private fun getVideoQualityFromResolution(resolution: Size?): VideoQuality =
/**
* Maps a given resolution [Size] to its corresponding [VideoQuality] based on the short edge.
*
* This classification is orientation-invariant, returning the same quality for both landscape and
* portrait orientations of the same resolution.
*/
internal fun getVideoQualityFromResolution(resolution: Size?): VideoQuality =
resolution?.let { res ->
val shortEdge = minOf(res.width, res.height)
QUALITY_RANGE_MAP.firstNotNullOfOrNull {
if (it.value.contains(res.height)) it.key else null
if (it.value.contains(shortEdge)) it.key else null
}
Comment thread
davidjiagoogle marked this conversation as resolved.
Resolved
} ?: VideoQuality.UNSPECIFIED

private fun getWidthFromCropRect(cropRect: Rect?): Int {
/**
* Returns the upright width of [cropRect] after applying [rotationDegrees].
*/
internal fun getWidthFromCropRect(cropRect: Rect?, rotationDegrees: Int): Int {
if (cropRect == null) {
return 0
}
return abs(cropRect.top - cropRect.bottom)
return if (rotationDegrees % 180 != 0) {
abs(cropRect.height())
} else {
abs(cropRect.width())
}
}

private fun getHeightFromCropRect(cropRect: Rect?): Int {
/**
* Returns the upright height of [cropRect] after applying [rotationDegrees].
*/
internal fun getHeightFromCropRect(cropRect: Rect?, rotationDegrees: Int): Int {
if (cropRect == null) {
return 0
}
return abs(cropRect.left - cropRect.right)
return if (rotationDegrees % 180 != 0) {
abs(cropRect.width())
} else {
abs(cropRect.height())
}
}

private fun createImageUseCase(
Expand Down Expand Up @@ -836,7 +884,8 @@ internal fun createVideoUseCase(
.setExecutor(backgroundDispatcher.asExecutor())
.apply {
videoQuality.toQuality()?.let { quality ->
// No fallback strategy is used. The app will crash if the quality is unsupported
// Allow falling back to a lower or higher supported quality rather than crashing
// if the requested quality cannot be satisfied by the active stream combination.
setQualitySelector(
QualitySelector.from(
quality,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 android.graphics.Rect
import android.util.Size
import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.model.VideoQuality
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class CameraSessionTest {

@Test
fun getVideoQualityFromResolution_landscapeResolutions_returnsExpectedQuality() {
assertThat(getVideoQualityFromResolution(Size(640, 480))).isEqualTo(VideoQuality.SD)
assertThat(getVideoQualityFromResolution(Size(1280, 720))).isEqualTo(VideoQuality.HD)
assertThat(getVideoQualityFromResolution(Size(1920, 1080))).isEqualTo(VideoQuality.FHD)
assertThat(getVideoQualityFromResolution(Size(2560, 1440))).isEqualTo(VideoQuality.FHD)
assertThat(getVideoQualityFromResolution(Size(3840, 2160))).isEqualTo(VideoQuality.UHD)
}

@Test
fun getVideoQualityFromResolution_portraitResolutions_returnsExpectedQuality() {
assertThat(getVideoQualityFromResolution(Size(480, 640))).isEqualTo(VideoQuality.SD)
assertThat(getVideoQualityFromResolution(Size(720, 1280))).isEqualTo(VideoQuality.HD)
assertThat(getVideoQualityFromResolution(Size(1080, 1920))).isEqualTo(VideoQuality.FHD)
assertThat(getVideoQualityFromResolution(Size(1440, 2560))).isEqualTo(VideoQuality.FHD)
assertThat(getVideoQualityFromResolution(Size(2160, 3840))).isEqualTo(VideoQuality.UHD)
}
Comment thread
davidjiagoogle marked this conversation as resolved.
Resolved

@Test
fun getVideoQualityFromResolution_rangeBoundaries_returnsExpectedQuality() {
assertThat(getVideoQualityFromResolution(Size(320, 240)))
.isEqualTo(VideoQuality.UNSPECIFIED)
assertThat(getVideoQualityFromResolution(Size(320, 241))).isEqualTo(VideoQuality.SD)
assertThat(getVideoQualityFromResolution(Size(1280, 719))).isEqualTo(VideoQuality.SD)
assertThat(getVideoQualityFromResolution(Size(1280, 720))).isEqualTo(VideoQuality.HD)
assertThat(getVideoQualityFromResolution(Size(1920, 1079))).isEqualTo(VideoQuality.HD)
assertThat(getVideoQualityFromResolution(Size(1920, 1080))).isEqualTo(VideoQuality.FHD)
assertThat(getVideoQualityFromResolution(Size(3840, 2159))).isEqualTo(VideoQuality.FHD)
assertThat(getVideoQualityFromResolution(Size(3840, 2160))).isEqualTo(VideoQuality.UHD)
assertThat(getVideoQualityFromResolution(Size(7680, 4319))).isEqualTo(VideoQuality.UHD)
assertThat(getVideoQualityFromResolution(Size(7680, 4320)))
.isEqualTo(VideoQuality.UNSPECIFIED)
}

@Test
fun getVideoQualityFromResolution_nullResolution_returnsUnspecified() {
assertThat(getVideoQualityFromResolution(null)).isEqualTo(VideoQuality.UNSPECIFIED)
}

@Test
fun cropRectDimensions_rotatedLandscapeCropRect_swapsToPortrait() {
val landscapeCropRect = Rect(0, 0, 1280, 720)
assertThat(getWidthFromCropRect(landscapeCropRect, rotationDegrees = 90)).isEqualTo(720)
assertThat(getHeightFromCropRect(landscapeCropRect, rotationDegrees = 90)).isEqualTo(1280)
assertThat(getWidthFromCropRect(landscapeCropRect, rotationDegrees = 270)).isEqualTo(720)
assertThat(getHeightFromCropRect(landscapeCropRect, rotationDegrees = 270)).isEqualTo(1280)
}

@Test
fun cropRectDimensions_unrotatedPortraitCropRect_preservesPortrait() {
val portraitCropRect = Rect(0, 0, 720, 1280)
assertThat(getWidthFromCropRect(portraitCropRect, rotationDegrees = 0)).isEqualTo(720)
assertThat(getHeightFromCropRect(portraitCropRect, rotationDegrees = 0)).isEqualTo(1280)
assertThat(getWidthFromCropRect(portraitCropRect, rotationDegrees = 180)).isEqualTo(720)
assertThat(getHeightFromCropRect(portraitCropRect, rotationDegrees = 180)).isEqualTo(1280)
}

@Test
fun cropRectDimensions_unrotatedLandscapeCropRect_preservesLandscape() {
val landscapeCropRect = Rect(0, 0, 1280, 720)
assertThat(getWidthFromCropRect(landscapeCropRect, rotationDegrees = 0)).isEqualTo(1280)
assertThat(getHeightFromCropRect(landscapeCropRect, rotationDegrees = 0)).isEqualTo(720)
assertThat(getWidthFromCropRect(landscapeCropRect, rotationDegrees = 180)).isEqualTo(1280)
assertThat(getHeightFromCropRect(landscapeCropRect, rotationDegrees = 180)).isEqualTo(720)
}

@Test
fun cropRectDimensions_nullCropRect_returnsZero() {
assertThat(getWidthFromCropRect(null, rotationDegrees = 0)).isEqualTo(0)
assertThat(getHeightFromCropRect(null, rotationDegrees = 0)).isEqualTo(0)
assertThat(getWidthFromCropRect(null, rotationDegrees = 90)).isEqualTo(0)
assertThat(getHeightFromCropRect(null, rotationDegrees = 90)).isEqualTo(0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.jetpackcamera.model.TestPattern
import kotlin.math.abs

private const val TAG = "DebugOverlayComponents"

Expand Down Expand Up @@ -305,7 +304,7 @@ private fun DebugDialogOptionsMenuDialog(
val videoResText = if (size == null) {
"null"
} else {
abs(size.height).toString() + "x" + abs(size.width).toString()
"${size.width}x${size.height}"
}
Text(
modifier = Modifier.testTag(
Expand Down
Loading