Skip to content
Draft
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 @@ -34,6 +34,7 @@ import com.google.jetpackcamera.core.camera.InternalCameraApi
import com.google.jetpackcamera.core.camera.PerpetualSessionSettings
import com.google.jetpackcamera.core.camera.appLensFacing
import com.google.jetpackcamera.core.camera.applyDeviceRotation
import com.google.jetpackcamera.core.camera.applyWindowRotation
import com.google.jetpackcamera.core.camera.attachPreviewSurfaceProvider
import com.google.jetpackcamera.core.camera.concurrentCompositionScaleX
import com.google.jetpackcamera.core.camera.createUseCaseGroup
Expand Down Expand Up @@ -147,7 +148,8 @@ internal suspend fun CameraSessionContext.runConcurrentCameraSession(
captureMode = sessionSettings.captureMode,
videoCaptureUseCase = videoCapture,
previewMirrorMode = sessionSettings.mirrorMode
.toCXConcurrentPreviewMirrorMode()
.toCXConcurrentPreviewMirrorMode(),
windowRotation = initialTransientSettings.windowRotation
Comment on lines 150 to +152

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The windowRotation argument is redundant here because createUseCaseGroup already defaults windowRotation to initialTransientSettings.windowRotation. We can omit it to simplify the call.

Suggested change
previewMirrorMode = sessionSettings.mirrorMode
.toCXConcurrentPreviewMirrorMode()
.toCXConcurrentPreviewMirrorMode(),
windowRotation = initialTransientSettings.windowRotation
previewMirrorMode = sessionSettings.mirrorMode.toCXConcurrentPreviewMirrorMode()

)
}.apply { attachPreviewSurfaceProvider(useCaseGroup = this) }

Expand Down Expand Up @@ -307,6 +309,7 @@ internal suspend fun CameraSessionContext.runConcurrentCameraSession(
}

applyDeviceRotation(initialTransientSettings.deviceRotation, useCaseGroup)
applyWindowRotation(initialTransientSettings.windowRotation, useCaseGroup)
processTransientSettingEvents(
primaryCamera,
perLensConstraints[primaryLensFacing],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ internal suspend fun runSingleCameraSession(
imageFormat = sessionSettings.imageFormat,
captureMode = sessionSettings.captureMode,
effect = effect,
captureResults = captureResults
captureResults = captureResults,
windowRotation = currentTransientSettings.windowRotation
Comment on lines +305 to +306

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The windowRotation argument is redundant here because createUseCaseGroup already defaults windowRotation to initialTransientSettings.windowRotation (which is currentTransientSettings.windowRotation in this call). We can omit it to simplify the call.

                            captureResults = captureResults

)
}.apply {
attachPreviewSurfaceProvider(useCaseGroup = this)
Expand Down Expand Up @@ -333,6 +334,7 @@ internal suspend fun runSingleCameraSession(
}

applyDeviceRotation(currentTransientSettings.deviceRotation, useCaseGroup)
applyWindowRotation(currentTransientSettings.windowRotation, useCaseGroup)

if (videoCaptureUseCase != null) {
val resolutionInfo = videoCaptureUseCase.resolutionInfo
Expand Down Expand Up @@ -613,6 +615,16 @@ suspend fun processTransientSettingEvents(
}
}

if (prevTransientSettings.windowRotation != newTransientSettings.windowRotation) {
Log.d(
TAG,
"Updating window rotation from " +
"${prevTransientSettings.windowRotation} -> " +
"${newTransientSettings.windowRotation}"
)
applyWindowRotation(newTransientSettings.windowRotation, useCaseGroup)
}

// setzoomratio when the primary zoom value changes.
if (prevTransientSettings.primaryLensFacing == newTransientSettings.primaryLensFacing &&
prevTransientSettings.zoomRatios[prevTransientSettings.primaryLensFacing] !=
Expand Down Expand Up @@ -740,11 +752,8 @@ fun CameraSessionContext.applyDeviceRotation(
useCaseGroup.useCases.forEach {
when (it) {
is Preview -> {
// Preview's target rotation should not be updated with device rotation.
// Instead, preview rotation should match the display rotation.
// When Preview is created, it is initialized with the display rotation.
// This will need to be updated separately if the display rotation is not
// locked. Currently the app is locked to portrait orientation.
// Preview's target rotation tracks TransientSessionSettings.windowRotation via
// applyWindowRotation rather than physical device gravity rotation.
}

is ImageCapture -> {
Expand All @@ -758,6 +767,27 @@ fun CameraSessionContext.applyDeviceRotation(
}
}

/**
* Updates [Preview.setTargetRotation] in place when [TransientSessionSettings.windowRotation]
* changes, without rebinding the camera session.
*
* Depends on CameraX fixes `aosp/4408335`, `aosp/4408336`, and `aosp/4408337`, which dynamically
* recalculate [ViewPort] and StreamSharing crop/rotation when [Preview.setTargetRotation] is
* updated on a bound use case.
*/
@InternalCameraApi
fun applyWindowRotation(
windowRotation: DeviceRotation,
useCaseGroup: UseCaseGroup
) {
val targetRotation = windowRotation.toUiSurfaceRotation()
useCaseGroup.useCases.forEach { useCase ->
if (useCase is Preview) {
useCase.targetRotation = targetRotation
}
}
Comment on lines +784 to +788

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using filterIsInstance<Preview>() is a more idiomatic and concise way to filter and iterate over specific use case types in Kotlin, aligning with the pattern already used elsewhere in this file (e.g., line 1047).

    useCaseGroup.useCases.filterIsInstance<Preview>().forEach { preview ->
        preview.targetRotation = targetRotation
    }
References
  1. Simplify Complex Logic: Look for needlessly complex code. If a multi-line block of logic can be condensed into a more concise and readable idiomatic expression (e.g., using Kotlin standard library functions), suggest the simplification. (link)

}

context(CameraSessionContext)
@InternalCameraApi
fun createUseCaseGroup(
Expand All @@ -770,15 +800,17 @@ fun createUseCaseGroup(
captureMode: CaptureMode,
effect: CameraEffect? = null,
captureResults: MutableStateFlow<TotalCaptureResult?>? = null,
previewMirrorMode: Int = CX_SINGLE_CAMERA_PREVIEW_MIRROR_MODE
previewMirrorMode: Int = CX_SINGLE_CAMERA_PREVIEW_MIRROR_MODE,
windowRotation: DeviceRotation = initialTransientSettings.windowRotation
): UseCaseGroup {
val previewUseCase =
createPreviewUseCase(
cameraInfo,
aspectRatio,
stabilizationMode,
previewMirrorMode,
captureResults
cameraInfo = cameraInfo,
aspectRatio = aspectRatio,
stabilizationMode = stabilizationMode,
previewMirrorMode = previewMirrorMode,
captureResults = captureResults,
windowRotation = windowRotation
)

// only create image use case in image or standard
Expand All @@ -799,12 +831,14 @@ fun createUseCaseGroup(
return UseCaseGroup.Builder().apply {
Log.d(
TAG,
"Setting initial device rotation to ${initialTransientSettings.deviceRotation}"
"Setting initial device rotation to ${initialTransientSettings.deviceRotation}, " +
"window rotation to $windowRotation"
)
// TODO(#592): Plumb windowRotation into PR #592's getViewPortAspectRatio /
// isPreviewLandscape once #592 merges into main.
setViewPort(
ViewPort.Builder(
Rational(aspectRatio.numerator, aspectRatio.denominator),
// Initialize rotation to Preview's rotation, which comes from Display rotation
previewUseCase.targetRotation
).build()
)
Expand All @@ -818,6 +852,12 @@ fun createUseCaseGroup(
}.build()
}

/**
* Applies [windowRotation] as the target rotation of this [Preview.Builder].
*/
internal fun Preview.Builder.applyWindowRotation(windowRotation: DeviceRotation): Preview.Builder =
setTargetRotation(windowRotation.toUiSurfaceRotation())

/**
* Maps a given resolution [Size] to its corresponding [VideoQuality] based on the short edge.
*
Expand Down Expand Up @@ -946,8 +986,11 @@ private fun createPreviewUseCase(
aspectRatio: AspectRatio,
stabilizationMode: StabilizationMode,
previewMirrorMode: Int,
captureResults: MutableStateFlow<TotalCaptureResult?>? = null
captureResults: MutableStateFlow<TotalCaptureResult?>? = null,
windowRotation: DeviceRotation = DeviceRotation.Natural
): Preview = Preview.Builder().apply {
applyWindowRotation(windowRotation)

updateCameraStateWithCaptureResults(
targetCameraInfo = cameraInfo,
captureResults = captureResults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ data class TransientSessionSettings(
val flashMode: FlashMode,
val primaryLensFacing: LensFacing,
val zoomRatios: Map<LensFacing, Float>,
val testPattern: TestPattern
val testPattern: TestPattern,
val windowRotation: DeviceRotation = DeviceRotation.Natural
)

data class InitialRecordingSettings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ interface CameraSystem {
*/
fun setDeviceRotation(deviceRotation: DeviceRotation)

/**
* Sets the host OS display/window rotation used to orient the live preview stream,
* [androidx.camera.core.ViewPort], and stream-sharing OpenGL output buffers.
*
* @param windowRotation The host display/window [DeviceRotation] to set.
*/
fun setWindowRotation(windowRotation: DeviceRotation) {}

/**
* Sets the image output format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -150,6 +151,7 @@ class CameraXCameraSystem(
private val videoCaptureControlEvents = Channel<VideoCaptureControlEvent>()

private val currentSettings = MutableStateFlow<CameraAppSettings?>(null)
private val currentWindowRotation = MutableStateFlow(DeviceRotation.Natural)
private var externalCaptureMode: ExternalCaptureMode = ExternalCaptureMode.Standard

// Per-lens default sub-modes that failed to bind during the current initialization cycle.
Expand Down Expand Up @@ -598,73 +600,75 @@ class CameraXCameraSystem(
clearCameraError()

val transientSettings = MutableStateFlow<TransientSessionSettings?>(null)
currentSettings
.filterNotNull()
.map { currentCameraSettings ->
transientSettings.value = TransientSessionSettings(
isAudioEnabled = currentCameraSettings.audioEnabled,
deviceRotation = currentCameraSettings.deviceRotation,
flashMode = currentCameraSettings.flashMode,
primaryLensFacing = currentCameraSettings.cameraLensFacing,
zoomRatios = currentCameraSettings.defaultZoomRatios,
testPattern = currentCameraSettings.debugSettings.testPattern
)
combine(
currentSettings.filterNotNull(),
currentWindowRotation
) { currentCameraSettings, windowRotation ->
transientSettings.value = TransientSessionSettings(
isAudioEnabled = currentCameraSettings.audioEnabled,
deviceRotation = currentCameraSettings.deviceRotation,
flashMode = currentCameraSettings.flashMode,
primaryLensFacing = currentCameraSettings.cameraLensFacing,
zoomRatios = currentCameraSettings.defaultZoomRatios,
testPattern = currentCameraSettings.debugSettings.testPattern,
windowRotation = windowRotation
)

val activeSubMode =
registeredSubModes[currentCameraSettings.activeCaptureSubModeId]
val activeSubModeBinding = activeSubMode?.provider?.sessionBinding
if (activeSubMode != null &&
activeSubModeBinding is CameraSessionBinding.Custom
) {
PerpetualSessionSettings.CustomSession(
subModeKey = activeSubMode.key,
aspectRatio = currentCameraSettings.aspectRatio,
captureMode = currentCameraSettings.captureMode,
mirrorMode = currentCameraSettings.mirrorMode
val activeSubMode =
registeredSubModes[currentCameraSettings.activeCaptureSubModeId]
val activeSubModeBinding = activeSubMode?.provider?.sessionBinding
if (activeSubMode != null &&
activeSubModeBinding is CameraSessionBinding.Custom
) {
PerpetualSessionSettings.CustomSession(
subModeKey = activeSubMode.key,
aspectRatio = currentCameraSettings.aspectRatio,
captureMode = currentCameraSettings.captureMode,
mirrorMode = currentCameraSettings.mirrorMode
)
} else {
val cameraConstraints =
systemConstraints.forCurrentLens(currentCameraSettings)
if (cameraConstraints == null) {
Log.e(
TAG,
"Could not retrieve constraints for " +
"${currentCameraSettings.cameraLensFacing}"
)
} else {
val cameraConstraints =
systemConstraints.forCurrentLens(currentCameraSettings)
if (cameraConstraints == null) {
Log.e(
TAG,
"Could not retrieve constraints for " +
"${currentCameraSettings.cameraLensFacing}"
currentCameraState.update { old ->
old.copy(
isCameraRunning = false,
cameraError = CameraError.CameraRemoved
)
currentCameraState.update { old ->
old.copy(
isCameraRunning = false,
cameraError = CameraError.CameraRemoved
)
}
return@map null
}
return@combine null
}

val resolvedStabilizationMode = resolveStabilizationMode(
requestedStabilizationMode = currentCameraSettings.stabilizationMode,
targetFrameRate = currentCameraSettings.targetFrameRate,
cameraConstraints = cameraConstraints
)

val activeCameraEffect = cameraEffectProviders.keys.firstOrNull {
it.id == currentCameraSettings.selectedCameraEffect
}
val resolvedStabilizationMode = resolveStabilizationMode(
requestedStabilizationMode = currentCameraSettings.stabilizationMode,
targetFrameRate = currentCameraSettings.targetFrameRate,
cameraConstraints = cameraConstraints
)

PerpetualSessionSettings.SingleCamera(
aspectRatio = currentCameraSettings.aspectRatio,
captureMode = currentCameraSettings.captureMode,
activeCameraEffect = activeCameraEffect,
targetFrameRate = currentCameraSettings.targetFrameRate,
stabilizationMode = resolvedStabilizationMode,
dynamicRange = currentCameraSettings.dynamicRange,
videoQuality = currentCameraSettings.videoQuality,
mirrorMode = currentCameraSettings.mirrorMode,
imageFormat = currentCameraSettings.imageFormat,
lowLightBoostPriority = currentCameraSettings.lowLightBoostPriority,
activeCaptureSubMode = activeSubMode?.key
)
val activeCameraEffect = cameraEffectProviders.keys.firstOrNull {
it.id == currentCameraSettings.selectedCameraEffect
}

PerpetualSessionSettings.SingleCamera(
aspectRatio = currentCameraSettings.aspectRatio,
captureMode = currentCameraSettings.captureMode,
activeCameraEffect = activeCameraEffect,
targetFrameRate = currentCameraSettings.targetFrameRate,
stabilizationMode = resolvedStabilizationMode,
dynamicRange = currentCameraSettings.dynamicRange,
videoQuality = currentCameraSettings.videoQuality,
mirrorMode = currentCameraSettings.mirrorMode,
imageFormat = currentCameraSettings.imageFormat,
lowLightBoostPriority = currentCameraSettings.lowLightBoostPriority,
activeCaptureSubMode = activeSubMode?.key
)
}
}
.distinctUntilChanged()
.collectLatest { sessionSettings ->
if (sessionSettings == null) return@collectLatest
Expand Down Expand Up @@ -1394,6 +1398,10 @@ class CameraXCameraSystem(
}
}

override fun setWindowRotation(windowRotation: DeviceRotation) {
currentWindowRotation.value = windowRotation
}

override suspend fun setImageFormat(imageFormat: ImageOutputFormat) {
currentSettings.update { old ->
old?.withUserChange { copy(imageFormat = imageFormat) }
Expand Down
Loading
Loading