From cfb30df448fb3229b3bf5b2baf279f3d2997c6cf Mon Sep 17 00:00:00 2001 From: David Jia Date: Wed, 23 Oct 2024 16:04:23 -0700 Subject: [PATCH] WIP --- .../jetpackcamera/core/camera/CameraExt.kt | 16 ++++++- .../core/camera/CameraSession.kt | 35 +++++++++++--- .../core/camera/CameraSessionSettings.kt | 2 + .../core/camera/CameraUseCase.kt | 3 ++ .../core/camera/CameraXCameraUseCase.kt | 48 ++++++++++++++++++- .../settings/model/CameraAppSettings.kt | 4 ++ .../settings/model/Constraints.kt | 4 ++ .../jetpackcamera/settings/SettingsUiState.kt | 12 ++++- .../settings/SettingsViewModel.kt | 4 +- .../settings/ui/SettingsComponents.kt | 14 ++++++ 10 files changed, 130 insertions(+), 12 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraExt.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraExt.kt index df24af54b..34bdeb597 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraExt.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraExt.kt @@ -17,22 +17,26 @@ package com.google.jetpackcamera.core.camera import android.annotation.SuppressLint import android.hardware.camera2.CameraCharacteristics +import android.util.Size import androidx.annotation.OptIn import androidx.camera.camera2.interop.Camera2CameraInfo import androidx.camera.camera2.interop.ExperimentalCamera2Interop import androidx.camera.core.CameraInfo import androidx.camera.core.CameraSelector -import androidx.camera.core.DynamicRange as CXDynamicRange import androidx.camera.core.ExperimentalImageCaptureOutputFormat import androidx.camera.core.ImageCapture import androidx.camera.core.Preview import androidx.camera.core.UseCase import androidx.camera.core.UseCaseGroup +import androidx.camera.video.Quality import androidx.camera.video.Recorder import androidx.camera.video.VideoCapture import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.ImageOutputFormat import com.google.jetpackcamera.settings.model.LensFacing +import java.util.Arrays +import java.util.Collections +import androidx.camera.core.DynamicRange as CXDynamicRange val CameraInfo.appLensFacing: LensFacing get() = when (this.lensFacing) { @@ -60,6 +64,16 @@ fun DynamicRange.toCXDynamicRange(): CXDynamicRange { } } +fun Quality.toSupportedSizes(): List { + return when (this) { + Quality.SD -> listOf(Size(720, 480), Size(640, 480)) + Quality.HD -> listOf(Size(1280, 720)) + Quality.FHD -> listOf(Size(1920, 1080)) + Quality.UHD -> listOf(Size(3840, 2160)) + else -> listOf() + } +} + fun LensFacing.toCameraSelector(): CameraSelector = when (this) { LensFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA LensFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA 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 a3ed19db2..6e691fa3b 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 @@ -16,6 +16,7 @@ package com.google.jetpackcamera.core.camera import android.Manifest +import android.annotation.SuppressLint import android.content.ContentValues import android.content.Context import android.content.pm.PackageManager @@ -29,6 +30,7 @@ import android.os.SystemClock import android.provider.MediaStore import android.util.Log import android.util.Range +import android.util.Size import androidx.annotation.OptIn import androidx.camera.camera2.interop.Camera2CameraInfo import androidx.camera.camera2.interop.Camera2Interop @@ -48,6 +50,7 @@ import androidx.camera.core.UseCaseGroup import androidx.camera.core.ViewPort import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.ResolutionSelector +import androidx.camera.core.resolutionselector.ResolutionStrategy import androidx.camera.video.FileDescriptorOutputOptions import androidx.camera.video.FileOutputOptions import androidx.camera.video.MediaStoreOutputOptions @@ -71,11 +74,6 @@ import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.ImageOutputFormat import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization -import java.io.File -import java.util.Date -import java.util.concurrent.Executor -import kotlin.coroutines.ContinuationInterceptor -import kotlin.math.abs import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineStart @@ -91,6 +89,11 @@ 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 private const val TAG = "CameraSession" @@ -116,6 +119,7 @@ internal suspend fun runSingleCameraSession( aspectRatio = sessionSettings.aspectRatio, targetFrameRate = sessionSettings.targetFrameRate, dynamicRange = sessionSettings.dynamicRange, + videoSize = sessionSettings.videoSize, imageFormat = sessionSettings.imageFormat, useCaseMode = useCaseMode, effect = when (sessionSettings.captureMode) { @@ -244,6 +248,7 @@ internal fun createUseCaseGroup( aspectRatio: AspectRatio, targetFrameRate: Int, dynamicRange: DynamicRange, + videoSize: Size? = null, imageFormat: ImageOutputFormat, useCaseMode: CameraUseCase.UseCaseMode, effect: CameraEffect? = null @@ -266,6 +271,7 @@ internal fun createUseCaseGroup( targetFrameRate, stabilizeVideoMode, dynamicRange, + videoSize, backgroundDispatcher ) } else { @@ -330,12 +336,14 @@ private fun createImageUseCase( return builder.build() } +@SuppressLint("RestrictedApi") private fun createVideoUseCase( cameraInfo: CameraInfo, aspectRatio: AspectRatio, targetFrameRate: Int, stabilizeVideoMode: Stabilization, dynamicRange: DynamicRange, + videoSize: Size?, backgroundDispatcher: CoroutineDispatcher ): VideoCapture { val sensorLandscapeRatio = cameraInfo.sensorLandscapeRatio @@ -355,6 +363,12 @@ private fun createVideoUseCase( } setDynamicRange(dynamicRange.toCXDynamicRange()) + + if (videoSize != null) { + setResolutionSelector( + getResolutionSelector(cameraInfo.sensorLandscapeRatio, aspectRatio) + ) + } }.build() } @@ -401,7 +415,8 @@ private fun createPreviewUseCase( private fun getResolutionSelector( sensorLandscapeRatio: Float, - aspectRatio: AspectRatio + aspectRatio: AspectRatio, + videoSize: Size? = null ): ResolutionSelector { val aspectRatioStrategy = when (aspectRatio) { AspectRatio.THREE_FOUR -> AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY @@ -419,7 +434,13 @@ private fun getResolutionSelector( } } } - return ResolutionSelector.Builder().setAspectRatioStrategy(aspectRatioStrategy).build() + val builder = ResolutionSelector.Builder().setAspectRatioStrategy(aspectRatioStrategy) + if (videoSize != null) { + builder.setResolutionStrategy( + ResolutionStrategy(videoSize, ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER) + ) + } + return builder.build() } context(CameraSessionContext) 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 b96c6a31d..91da98517 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 @@ -15,6 +15,7 @@ */ package com.google.jetpackcamera.core.camera +import android.util.Size import androidx.camera.core.CameraInfo import com.google.jetpackcamera.settings.model.AspectRatio import com.google.jetpackcamera.settings.model.CaptureMode @@ -41,6 +42,7 @@ internal sealed interface PerpetualSessionSettings { val stabilizePreviewMode: Stabilization, val stabilizeVideoMode: Stabilization, val dynamicRange: DynamicRange, + val videoSize: Size?, val imageFormat: ImageOutputFormat ) : PerpetualSessionSettings 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 1cf2e4e6e..8783aacb8 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 @@ -17,6 +17,7 @@ package com.google.jetpackcamera.core.camera import android.content.ContentResolver import android.net.Uri +import android.util.Size import androidx.camera.core.ImageCapture import androidx.camera.core.SurfaceRequest import com.google.jetpackcamera.settings.model.AspectRatio @@ -96,6 +97,8 @@ interface CameraUseCase { suspend fun setAspectRatio(aspectRatio: AspectRatio) + suspend fun setVideoSize(size: Size) + suspend fun setLensFacing(lensFacing: LensFacing) suspend fun tapToFocus(x: Float, y: Float) 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 20ecf25b7..295cbbd24 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 @@ -23,6 +23,7 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import android.util.Log +import android.util.Size import androidx.camera.core.CameraInfo import androidx.camera.core.CameraSelector import androidx.camera.core.DynamicRange as CXDynamicRange @@ -146,10 +147,21 @@ constructor( for (lensFacing in availableCameraLenses) { val selector = lensFacing.toCameraSelector() selector.filter(availableCameraInfos).firstOrNull()?.let { camInfo -> + val videoCapabilities = Recorder.getVideoCapabilities(camInfo) val supportedDynamicRanges = - Recorder.getVideoCapabilities(camInfo).supportedDynamicRanges + videoCapabilities.supportedDynamicRanges .mapNotNull(CXDynamicRange::toSupportedAppDynamicRange) .toSet() + val supportedVideoSizesMap = mutableMapOf>() + for (dynamicRange in supportedDynamicRanges) { + val supportedSizes = mutableListOf() + for (quality in videoCapabilities.getSupportedQualities( + dynamicRange.toCXDynamicRange() + )) { + supportedSizes.addAll(quality.toSupportedSizes()) + } + supportedVideoSizesMap[dynamicRange] = supportedSizes + } val supportedStabilizationModes = buildSet { if (camInfo.isPreviewStabilizationSupported) { @@ -179,6 +191,7 @@ constructor( Pair(CaptureMode.SINGLE_STREAM, setOf(ImageOutputFormat.JPEG)), Pair(CaptureMode.MULTI_STREAM, supportedImageFormats) ), + supportedVideoSizesMap = supportedVideoSizesMap, hasFlashUnit = hasFlashUnit ) ) @@ -197,6 +210,7 @@ constructor( .tryApplyFrameRateConstraints() .tryApplyStabilizationConstraints() .tryApplyConcurrentCameraModeConstraints() + .tryApplyVideoSizeConstraints() if (isDebugMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { withContext(iODispatcher) { val cameraProperties = @@ -242,6 +256,7 @@ constructor( stabilizePreviewMode = currentCameraSettings.previewStabilization, stabilizeVideoMode = currentCameraSettings.videoCaptureStabilization, dynamicRange = currentCameraSettings.dynamicRange, + videoSize = currentCameraSettings.videoSize, imageFormat = currentCameraSettings.imageFormat ) } @@ -556,6 +571,28 @@ constructor( } } + private fun CameraAppSettings.tryApplyVideoSizeConstraints(): CameraAppSettings { + return systemConstraints.perLensConstraints[cameraLensFacing]?.let { constraints -> + with(constraints.supportedVideoSizesMap) { + val newVideoSize = if (contains(dynamicRange) && + !get(dynamicRange).isNullOrEmpty() + ) { + if (get(dynamicRange)!!.contains(videoSize)) { + videoSize + } else { + get(dynamicRange)!![0] + } + } else { + null + } + + this@tryApplyVideoSizeConstraints.copy( + videoSize = newVideoSize + ) + } + } ?: this + } + override suspend fun tapToFocus(x: Float, y: Float) { focusMeteringEvents.send(CameraEvent.FocusMeteringEvent(x, y)) } @@ -579,6 +616,14 @@ constructor( } } + override suspend fun setVideoSize(size: Size) { + currentSettings.update { old -> + old?.copy(videoSize = size) + ?.tryApplyVideoSizeConstraints() + } + // TODO: Update UseCases + } + override suspend fun setCaptureMode(captureMode: CaptureMode) { currentSettings.update { old -> old?.copy(captureMode = captureMode) @@ -591,6 +636,7 @@ constructor( currentSettings.update { old -> old?.copy(dynamicRange = dynamicRange) ?.tryApplyConcurrentCameraModeConstraints() + ?.tryApplyVideoSizeConstraints() } } 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 4335e8824..9d185512d 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 @@ -14,6 +14,9 @@ * limitations under the License. */ package com.google.jetpackcamera.settings.model + +import android.util.Size + const val TARGET_FPS_AUTO = 0 const val UNLIMITED_VIDEO_DURATION = 0L @@ -29,6 +32,7 @@ data class CameraAppSettings( val previewStabilization: Stabilization = Stabilization.UNDEFINED, val videoCaptureStabilization: Stabilization = Stabilization.UNDEFINED, val dynamicRange: DynamicRange = DynamicRange.SDR, + val videoSize: Size? = null, val defaultHdrDynamicRange: DynamicRange = DynamicRange.HLG10, val defaultHdrImageOutputFormat: ImageOutputFormat = ImageOutputFormat.JPEG_ULTRA_HDR, val lowLightBoost: LowLightBoost = LowLightBoost.DISABLED, diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt index 8b75351d9..2c9924c4d 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt @@ -15,6 +15,8 @@ */ package com.google.jetpackcamera.settings.model +import android.util.Size + data class SystemConstraints( val availableLenses: List, val concurrentCamerasSupported: Boolean, @@ -26,6 +28,7 @@ data class CameraConstraints( val supportedFixedFrameRates: Set, val supportedDynamicRanges: Set, val supportedImageFormatsMap: Map>, + val supportedVideoSizesMap: Map>, val hasFlashUnit: Boolean ) @@ -48,6 +51,7 @@ val TYPICAL_SYSTEM_CONSTRAINTS = Pair(CaptureMode.SINGLE_STREAM, setOf(ImageOutputFormat.JPEG)), Pair(CaptureMode.MULTI_STREAM, setOf(ImageOutputFormat.JPEG)) ), + supportedVideoSizesMap = emptyMap(), hasFlashUnit = lensFacing == LensFacing.BACK ) ) 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 737c5d579..6531216c0 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 @@ -18,12 +18,16 @@ package com.google.jetpackcamera.settings import com.google.jetpackcamera.settings.DisabledRationale.DeviceUnsupportedRationale import com.google.jetpackcamera.settings.DisabledRationale.LensUnsupportedRationale import com.google.jetpackcamera.settings.model.AspectRatio +import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.CaptureMode import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import com.google.jetpackcamera.settings.model.DarkMode +import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization +import com.google.jetpackcamera.settings.model.SystemConstraints +import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS import com.google.jetpackcamera.settings.ui.DEVICE_UNSUPPORTED_TAG import com.google.jetpackcamera.settings.ui.FPS_UNSUPPORTED_TAG import com.google.jetpackcamera.settings.ui.LENS_UNSUPPORTED_TAG @@ -54,7 +58,9 @@ sealed interface SettingsUiState { val fpsUiState: FpsUiState, val lensFlipUiState: FlipLensUiState, val stabilizationUiState: StabilizationUiState, - val maxVideoDurationUiState: MaxVideoDurationUiState.Enabled + val maxVideoDurationUiState: MaxVideoDurationUiState.Enabled, + val cameraAppSettings: CameraAppSettings, + val systemConstraints: SystemConstraints ) : SettingsUiState } @@ -230,5 +236,7 @@ val TYPICAL_SETTINGS_UISTATE = SettingsUiState.Enabled( stabilizationUiState = StabilizationUiState.Disabled( DeviceUnsupportedRationale(R.string.stabilization_rationale_prefix) - ) + ), + cameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS, + systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS ) diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt index 083916605..19c4f0038 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/SettingsViewModel.kt @@ -66,7 +66,9 @@ class SettingsViewModel @Inject constructor( darkModeUiState = DarkModeUiState.Enabled(updatedSettings.darkMode), fpsUiState = getFpsUiState(constraints, updatedSettings), lensFlipUiState = getLensFlipUiState(constraints, updatedSettings), - stabilizationUiState = getStabilizationUiState(constraints, updatedSettings) + stabilizationUiState = getStabilizationUiState(constraints, updatedSettings), + cameraAppSettings = updatedSettings, + systemConstraints = constraints ) }.stateIn( scope = viewModelScope, diff --git a/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt index 56a83838f..dc66448c2 100644 --- a/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt +++ b/feature/settings/src/main/java/com/google/jetpackcamera/settings/ui/SettingsComponents.kt @@ -16,6 +16,7 @@ package com.google.jetpackcamera.settings.ui import android.content.res.Configuration +import android.util.Size import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -73,11 +74,16 @@ import com.google.jetpackcamera.settings.TEN_SECONDS_DURATION import com.google.jetpackcamera.settings.THIRTY_SECONDS_DURATION import com.google.jetpackcamera.settings.UNLIMITED_VIDEO_DURATION 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.CaptureMode import com.google.jetpackcamera.settings.model.DarkMode +import com.google.jetpackcamera.settings.model.DynamicRange import com.google.jetpackcamera.settings.model.FlashMode import com.google.jetpackcamera.settings.model.LensFacing import com.google.jetpackcamera.settings.model.Stabilization +import com.google.jetpackcamera.settings.model.SystemConstraints +import com.google.jetpackcamera.settings.model.forCurrentLens import com.google.jetpackcamera.settings.ui.theme.SettingsPreviewTheme /** @@ -622,6 +628,14 @@ fun StabilizationSetting( ) } +@Composable +fun VideoSizeSetting(currentCameraSettings: CameraAppSettings, systemConstraints: SystemConstraints) { + val cameraConstraints = systemConstraints.forCurrentLens(currentCameraSettings) + val supportedSizes: List? = cameraConstraints?.supportedVideoSizesMap?.get(currentCameraSettings.dynamicRange) + //TODO: Use BasicPopupSetting with contents being supportedSizes + +} + @Composable fun VersionInfo(versionName: String, modifier: Modifier = Modifier, buildType: String = "") { SettingUI(